Collectable

Learn and implement the Collectable built-in protocol.

We'll cover the following

Introduction

We’ve already seen Enum.into. It takes something that’s enumerable and creates a new collection from it:

iex> 1..4 |> Enum.into([])
[1, 2, 3, 4]
iex> [ {1, 2}, {"a", "b"}] |> Enum.into(%{}) 
%{1 => 2, "a" => "b"}

The target of Enum.into must implement the Collectable protocol. This defines a single function, somewhat confusingly also called into. This function returns a two-element tuple. The first element is the initial value of the target collection. The second is a function to be called to add each item to the collection.

If this seems similar to the second and third parameters passed to Enum.reduce, that’s because in a way, the into function is the opposite of reduce.

Let’s look at the code first:

defimpl Collectable, for: Midi do
  use Bitwise

  def into(%Midi{content: content}) do
    {
      content,
      fn
        acc, {:cont, frame = %Midi.Frame{}} ->
          acc <> Midi.Frame.to_binary(frame)

        acc,  :done ->
          %Midi{content: acc}

        _, :halt ->
          :ok
      end
    }
  end
end

It works like this:

  • The Enum.into command calls the into function for Midi, passing it the target value, which is Midi{con- tent: content} in this case.
  • The Midi.into command returns a tuple. The first element is the current content of the target. This acts as the initial value for an accumulator. The second element of the tuple is a function.
  • The Enum.into command then calls this function, passing it the accumulator and a command. If the command is :done, the iteration over the collection being injected into the MIDI stream has finished, so we return a new Midi structure using the accumulator as a value. If the command is :halt, the iteration has terminated early and nothing needs to be done.
  • The real work is done when the function is passed the {:cont, frame} command. Here’s where the Collectable protocol appends the binary representation of the next frame to the accumulator.

Because the into function uses the initial value of the target collection, we can use it to append to a MIDI stream:

iex> midi2 = %Midi{}
%Midi{content: ""}
iex> midi2 = Enum.take(midi, 1) |> Enum.into(midi2)
%Midi{content: <<77, 84, 104, 100, 0, 0, 0, 6, 0, 1, 0, 8, 0, 120>>} 
iex> Enum.count(midi2) 
1

We need to run the following commands to execute midi2 = %Midi{} because we need midi in our code.

  • midi = Midi.from_file("dueling-banjos.mid")
  • Enum.take(midi, 2)
  • r Enumerable.Midi

Remember the big picture

If all this Enumerable and Collectable stuff seems complicated, that’s because it is. In part, that’s because these conventions allow all enumerable values to be used both eagerly and lazily. When we’re dealing with big (or even infinite) collections, this is a big deal.

Get hands-on with 1200+ tech skills courses.