Do you want to pick up from where you left of?
Take me there

Protocols

In this lesson we are going to look at Protocols, what they are, and how we use them in Elixir.

What Are Protocols

So what are they? Protocols are a means of achieving polymorphism in Elixir. One pain of Erlang is extending an existing API for newly defined types. To avoid this in Elixir the function is dispatched dynamically based on the value’s type. Elixir comes with a number of protocols built in, for example the String.Chars protocol is responsible for the to_string/1 function we’ve seen used previously. Let’s take a closer look at to_string/1 with a quick example:

iex> to_string(5)
"5"
iex> to_string(12.4)
"12.4"
iex> to_string("foo")
"foo"

As you can see we’ve called the function on multiple types and demonstrated that it works on them all. What if we call to_string/1 on tuples (or any type that hasn’t implemented String.Chars)? Let’s see:

to_string({:foo})
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:foo}
    (elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
    (elixir) lib/string/chars.ex:17: String.Chars.to_string/1

As you can see we get a protocol error as there is no implementation for tuples. In the next section we’ll implement the String.Chars protocol for tuples.

Implementing a protocol

We saw that to_string/1 has not yet been implemented for tuples, so let’s add it. To create an implementation we’ll use defimpl with our protocol, and provide the :for option, and our type. Let’s take a look at how it might look:

defimpl String.Chars, for: Tuple do
  def to_string(tuple) do
    interior =
      tuple
      |> Tuple.to_list()
      |> Enum.map(&Kernel.to_string/1)
      |> Enum.join(", ")

    "{#{interior}}"
  end
end

If we copy this into IEx we should be now be able to call to_string/1 on a tuple without getting an error:

iex> to_string({3.14, "apple", :pie})
"{3.14, apple, pie}"

We know how to implement a protocol but how do we define a new one? For our example we’ll implement to_atom/1. Let’s see how to do that with defprotocol:

defprotocol AsAtom do
  def to_atom(data)
end

defimpl AsAtom, for: Atom do
  def to_atom(atom), do: atom
end

defimpl AsAtom, for: BitString do
  defdelegate to_atom(string), to: String
end

defimpl AsAtom, for: List do
  defdelegate to_atom(list), to: List
end

defimpl AsAtom, for: Map do
  def to_atom(map), do: List.first(Map.keys(map))
end

Here we’ve defined our protocol and its expected function, to_atom/1, along with implementations for a few types. Now that we have our protocol, let’s put it to use in IEx:

iex> import AsAtom
AsAtom
iex> to_atom("string")
:string
iex> to_atom(:an_atom)
:an_atom
iex> to_atom([1, 2])
:"\x01\x02"
iex> to_atom(%{foo: "bar"})
:foo

It is worth it to note, that although underneath structs are Maps, they do not share protocol implementations with Maps. They are not enumerable, they cannot be accessed.

As we can see, protocols are a powerful way to achieve polymorphism.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!