Elixir protocols in action

In a previous post, I discussed the use of Protocols in Elixir. In effect, I was struggling for an internal definition for myself as to the difference between a behaviour and a protocol, until I came across an article by Gregory Brown which has this quote from ‘Growing Object Oriented Software’:

An interface defines whether two things can fit together, a protocol defines whether two things can work together.

To demonstrate how a protocol allows a custom struct to work with the Enum module, I’ve devised a contrived example whereby I’m implementing the Collectable protocol for a custom data struct.

The Enum module supports a function into which takes an enumerable, and a data structure and transposes the original enumerable into the given data structure and returns it.

An example of putting items from one list into another after transformation using Enum.into/3:

1
Enum.into([1,2,3], [], fn(x) -> x + 1 end) # => [2,3,4]

The second argument for into is our Collectable as specified in the docs. We just need to implement the Collectable protocol for our data struct by implementing one method called into and passing it the original data structure.

Assuming we have a struct called MyCollectable, the implementation and function declaration looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# this is the custom data struct we are storing the data into
defmodule MyCollectable do
  defstruct results: []
end


defimpl Collectable, for: MyCollectable do
  def into(original) do
    {original, fn
      source, {:cont, nil} -> source
      source, {:cont, value} ->
        %MyCollectable{source | results: [value|source.results]}
      source, :done ->
        %MyCollectable{source | results: :lists.reverse(source.results)}
      _, :halt -> :ok
    end}
  end
end

The Collectable.into function returns a tuple of the original collection and a function which is applied as it cycles through each item.

For {:cont, nil} no value is matched so it returns the original collection.

For {:cont, value} a value is found so it is appended to the head of the collection results list in our example but can be customized to suit.

When {:done} is read, it means it has reached the end of the iteration. In our case, we return an updated MyCollectable struct with the results reversed in the right sequence.

Finally, {:halt} is implemented to return :ok and it is to handle situations where processing is interuppted.

To use it, we can pass it into Enum.into like so:

1
2
3
Enum.into([1,2,3], %MyCollectable{}) # => %MyCollectable{results: [1,2,3]}

Enum.into([1,2,3], %MyCollectable{}, fn x -> x + 1 end ) # => %MyCollectable{results: [2,3,4]}

A sample github repository for this post is available.

Keep hacking and stay curious!!

Further information