View Source Collectable protocol (Elixir v1.11.4)

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 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 just the range limits 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.


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

iex> {initial_acc, collector_fun} = Collectable.into(
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<[1, 2, 3]>

To show how the protocol can be implemented, we can again look at the 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(original) do
    collector_fun = fn
      set, {:cont, elem} -> MapSet.put(set, elem)
      set, :done -> set
      _set, :halt -> :ok

    {original, collector_fun}

Link to this section Summary


Returns an initial accumulator and a "collector" function.

Link to this section Types

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

Link to this section Functions

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

Returns an initial accumulator and a "collector" function.

The returned function receives a term and a command and injects the term into the collectable 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.