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 theinto
function forMidi
, passing it the target value, which isMidi{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 newMidi
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 theCollectable
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 1400+ tech skills courses.