Protocols

Protocols are a mechanism to achieve polymorphism in Elixir. Dispatching on a protocol is available to any data type as long as it implements the protocol.

In other words you can write a function that behaves differently depending on the type of the first argument to it’s functions. Protocol implementations can be supplied for one of the built in supported aliases Atom, BitString, Float, Function, Integer, List, Map, PID, Port, Reference, Tuple, and Any; or a user defined struct. Lets make a Countable protocol, a contrived example that counts items:

The first step is to define a protocol:

iex> defprotocol Countable do...> def count_items(term)...> end

Notice that we are defining a function in the Countable protocol without a body. Protocols only support definition headers, in fact, they redefine the def macro. Definition headers are a little more restrictive, for example a definition with a guard clause will not compile:

Notice that the for argument can be omitted when defining an implementation inside a module that also defines a struct.

Although there are restrictions when defining the protocol when it comes to matching, there are no such restrictions when implementing the protocol, we can use pattern matching and guards freely. This also means multiple function bodies for the same arity can be defined.

It is now possible to invoke the protocol function with an argument that matches the types above:

Interestingly each implementation will become a sub-module of the protocol module, which means module attributes are supported in a Protocol implementation. We can prove this by looking at some module info:

The @for module attribute

Protocol implementations have access to the @for module attribute that is the alias of the current target, and the @protocol module attribute that is the alias of the protocol being implemented. This can come in handy when implementing a protocol for multiple aliases:

Note: It is possible to derive the Any protocol implementation if it is necessary to whitelist the modules that can fallback.

Built-in protocols

There are a number of built-in protocols that may prove useful to implement in your own code. At the time of writing these are Collectable, Enumerable, Inspect, List.Chars and String.Chars. I think it is telling that all of the built-in protocols only have one function with the exception of Enumerable which only has three. Where possible it is best to keep the "surface area" of a protocol small.

When to use them

The great advantage of protocols is that the implementation of a protocol can be outside of the library / application that defines it. This is particularly handy for library authors to create extension points for the consumers own types. Some examples from packages out there might help illustrate this:

Plug.Exception: A protocol that extends exceptions to be status-code aware

Phoenix.Param: A protocol that converts data structures into URL parameters

Performance

There is a potential performance hit when using protocols; from the guide:

Because a protocol can dispatch to any data type, the protocol must check on every call if an implementation for the given type exists. This may be expensive.

This is why, as part of compilation, protocol consolidation is on by default. Protocol consolidation is the process of optimising dispatches by looking at all of the implementations in a project / application. To check if the option is on in your project you can run:

iex> Mix.Project.config[:consolidate_protocols]true

Wrapping Up

Protocols provide a great way to add extension points to your code, particularly for library authors. It is important not to go protocol crazy though, you shouldn’t use protocols when some functions on a module with pattern matching will do. The fact that there are only six built-in to the language is a good indicator of this.