cult3

What are Elixir Protocols?

Jun 27, 2016

Table of contents:

  1. What is polymorphism?
  2. How is polymorphism implemented in Elixir?
  3. Defining a Protocol in Elixir
  4. Implementing a Protocol in Elixir
  5. Implementing a Protocol for Structs
  6. Conclusion

An important concept in programming is “polymorphism”. You may have already come across the term if you have previous experience with another programming language as just about every programming language has this concept in one form or another.

Polymorphism is defined in Elixir using Protocols. We’ve already touched upon this concept a couple of times so far in our exploration of the language, but without ever really studying it to understand what is going on.

And you’ve probably already used polymorphism in Elixir without even knowing.

In today’s tutorial we will be looking into polymorphism and using Protocols in Elixir.

What is polymorphism?

Before we look at using Protocols in Elixir, first it’s important to have a solid grasp of polymorphism and why it is such an important concept.

Polymorphism is “the condition of occurring in several different forms”.

In programming this means you can usually act on something in a generic way, without knowing specifically what the thing is.

For example, you can print something as a string, without knowing what the thing is. The thing could be a number, or an integer, or a float. It doesn’t matter what the type is because each type knows what to do when it is converted to a string:

to_string("Hello World")
# "Hello World"

to_string(123)
# "123"

to_string(99.9)
# "99.9"

As long as the thing you are acting on knows how to handle the action, you’re good to go. This is polymorphism because it doesn’t matter what the thing is, as long as it responds correctly.

How is polymorphism implemented in Elixir?

So hopefully now you understand that polymorphism is basically just the ability to act in a generic way. Now let’s take a look at how polymorphism is implemented in Elixir.

We can get our first clue of how it is implement if we try to use the to_string/1 function on a tuple Using Tuples in Elixir):

to_string({:hello, :world})
# ** (Protocol.UndefinedError) protocol String.Chars not implemented for {:hello, :world}

Instead of printing the tuple as a string we got a protocol String.Chars not implemented error.

Under the hood, the to_string/1 function is using the String.Chars Protocol. The Protocol is like a blueprint that defines the function.

Each type that wants to implement the Protocol also has an implementation that deals with how the Protocol function should be handled.

The reason why the string, number and float calls to to_string/1 from earlier worked is because those types have implementations for the String.Chars Protocol.

However, the tuple type does not have an implementation for this Protocol.

Hopefully that makes sense. If not don’t worry, next up we’ll look at defining and implementing our own Protocol. Once you see behind the scenes I’m sure the pieces will start to fall into place.

Defining a Protocol in Elixir

Let’s now take a look at defining a Protocol in Elixir for checking if something is empty:

defprotocol Empty do
  def empty?(data)
end

As you can see, we don’t need to define a body for the function. If you are familiar with interfaces in other programming languages, you can think of a Protocol as essentially the same thing.

So this Protocol is saying that anything that implements it must have an empty? function, although it is up to the implementor as to how the function responds.

With the protocol defined, lets take a look at adding a couple of implementations.

Implementing a Protocol in Elixir

Now that we have the Protocol defined we can add implementations so we can actually use it.

The first implementation we will write will be for the List type as this function actually makes the most sense to use with lists:

defimpl Empty, for: List do
  def empty?([]), do: true
  def empty?(_), do: false
end

As you can see, you define the implementation by providing the type as the for argument.

You then implement the function as you see fit for the type.

In this case I’m using pattern matching and multi-clause functions to define two implementations, one for empty lists, and one for non-empty lists.

With the implementation defined, we can now use this Protocol in our code:

Empty.empty?([])
# true

Empty.empty?([1, 2, 3])
# false

You can also implement the Protocol for other types as you see fit:

defimpl Empty, for: Map do
  def empty?(map), do: map_size(map) == 0
end

defimpl Empty, for: BitString do
  def empty?(""), do: true
  def empty?(_), do: false
end

defimpl Empty, for: Atom do
  def empty?(false), do: true
  def empty?(nil), do: true
  def empty?(_), do: false
end

Here I’ve implemented the Protocol for Maps, Strings, and Atoms.

You can implement your Protocol for as many or as few types as you want, whatever makes sense for the usage of your Protocol.

Any types that attempt to use the Protocol without an implementation will raise a protocol not implemented error just like we saw earlier when we used the to_string/1 function on a tuple.

Implementing a Protocol for Structs

Last week we looked at using Structs in Elixir and we saw that despite the fact that Structs are Maps, you can’t use the functions from the Enum module on Structs.

This is because Structs do not share the Map implementations of Protocols.

Earlier we added a Map implementation for our Empty.empty? Protocol.

Empty.empty?(%{})
# true

If we now define a new Struct, we can see that this Struct does not share the Map Protocol implementations:

# Define the Struct
defmodule Cart do
  defstruct id: nil, items: []
end

# Create a new Struct
cart = %Cart{id: 123}

# Check if the Struct is empty
Empty.empty?(cart)
# ** (Protocol.UndefinedError) protocol Empty not implemented for %Cart{id: 123}

So as you can see, the Protocol is definitely not implemented for the Cart despite it being just a special type of Map.

Now we can add an implementation of the Empty.empty? protocol for the Cart Struct:

defimpl Empty, for: Cart do
  def empty?(%{items: []}), do: true
  def empty?(_), do: false
end

Here pattern matching for an empty items list and returning false, otherwise I’m returning true.

Now we can check to make sure this works as it should:

cart1 = %Cart{id: 123}
# %Cart{id: 123, items: []}

Empty.empty?(cart1)
# true

cart2 = %Cart{id: 123, items: [:milk]}
# %Cart{id: 123, items: [:milk]}

Empty.empty?(cart2)
# false

And as you can see, the Empty.empty? Protocol is working correctly!

Conclusion

Protocols and polymorphism is a very important topic in Elixir. As you can probably see from the very first example, being able to use the to_string/1 function on multiple different types and getting something back for each is very useful.

Protocols are also very important when it comes to defining your own Structs. Structs are much more than just named maps with known values. Using Structs and Protocols hand-in-hand will allow you to define powerful behaviour as part of your application.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.