View Source Collectable protocol (Elixir v1.15.3)

A protocol to traverse data structures.

The Enum.into/2 function uses this protocol to insert an enumerable into a collection:

iex> Enum.into([a: 1, b: 2], %{})
%{a: 1, b: 2}

Why Collectable?

The Enumerable protocol is useful to take values out of a collection. In order to support a wide range of values, the functions provided by the Enumerable protocol do not keep shape. For example, passing a map to Enum.map/2 always returns a list.

This design is intentional. Enumerable was designed to support infinite collections, resources and other structures with fixed shape. For example, it doesn't make sense to insert values into a Range, as it has a fixed shape where only the range limits and step are stored.

The Collectable module was designed to fill the gap left by the Enumerable protocol. Collectable.into/1 can be seen as the opposite of Enumerable.reduce/3. If the functions in Enumerable are about taking values out, then Collectable.into/1 is about collecting those values into a structure.

Examples

To show how to manually use the Collectable protocol, let's play with a simplified implementation for MapSet.

iex> {initial_acc, collector_fun} = Collectable.into(MapSet.new())
iex> updated_acc = Enum.reduce([1, 2, 3], initial_acc, fn elem, acc ->
...>   collector_fun.(acc, {:cont, elem})
...> end)
iex> collector_fun.(updated_acc, :done)
MapSet.new([1, 2, 3])

To show how the protocol can be implemented, we can again look at the simplified implementation for MapSet. In this implementation "collecting" elements simply means inserting them in the set through MapSet.put/2.

defimpl Collectable, for: MapSet do
  def into(map_set) do
    collector_fun = fn
      map_set_acc, {:cont, elem} ->
        MapSet.put(map_set_acc, elem)

      map_set_acc, :done ->
        map_set_acc

      _map_set_acc, :halt ->
        :ok
    end

    initial_acc = map_set

    {initial_acc, collector_fun}
  end
end

So now we can call Enum.into/2:

iex> Enum.into([1, 2, 3], MapSet.new())
MapSet.new([1, 2, 3])

Summary

Types

t()

All the types that implement this protocol.

Functions

Returns an initial accumulator and a "collector" function.

Types

@type command() :: {:cont, term()} | :done | :halt
@type t() :: term()

All the types that implement this protocol.

Functions

@spec into(t()) ::
  {initial_acc :: term(), collector :: (term(), command() -> t() | term())}

Returns an initial accumulator and a "collector" function.

Receives a collectable which can be used as the initial accumulator that will be passed to the function.

The collector function receives a term and a command and injects the term into the collectable accumulator on every {:cont, term} command.

:done is passed as a command when no further values will be injected. This is useful when there's a need to close resources or normalizing values. A collectable must be returned when the command is :done.

If injection is suddenly interrupted, :halt is passed and the function can return any value as it won't be used.

For examples on how to use the Collectable protocol and into/1 see the module documentation.