16 Protocols¶
{% include toc.html %}
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. Let’s see an example.
In Elixir, only false
and nil
are treated as false. Everything
else evaluates to true. Depending on the application, it may be
important to specify a blank?
protocol that returns a boolean for
other data types that should be considered blank. For instance, an empty
list or an empty binary could be considered blanks.
We could define this protocol as follows:
defprotocol Blank do
@doc "Returns true if data is considered blank/empty"
def blank?(data)
end
The protocol expects a function called blank?
that receives one
argument to be implemented. We can implement this protocol for different
Elixir data types as follows:
# Integers are never blank
defimpl Blank, for: Integer do
def blank?(_), do: false
end
# Just empty list is blank
defimpl Blank, for: List do
def blank?([]), do: true
def blank?(_), do: false
end
# Just empty map is blank
defimpl Blank, for: Map do
# Keep in mind we could not pattern match on %{} because
# it matches on all maps. We can however check if the size
# is zero (and size is a fast operation).
def blank?(map), do: map_size(map) == 0
end
# Just the atoms false and nil are blank
defimpl Blank, for: Atom do
def blank?(false), do: true
def blank?(nil), do: true
def blank?(_), do: false
end
And we would do so for all native data types. The types available are:
Atom
BitString
Float
Function
Integer
List
Map
PID
Port
Reference
Tuple
Now with the protocol defined and implementations in hand, we can invoke it:
iex> Blank.blank?(0)
false
iex> Blank.blank?([])
true
iex> Blank.blank?([1, 2, 3])
false
Passing a data type that does not implement the protocol raises an error:
iex> Blank.blank?("hello")
** (Protocol.UndefinedError) protocol Blank not implemented for "hello"
16.1 Protocols and structs¶
The power of Elixir’s extensibility comes when protocols and structs are used together.
In the previous chapter, we have learned that although structs are maps,
they do not share protocol implementations with maps. Let’s define a
User
struct as in the previous chapter:
iex> defmodule User do
...> defstruct name: "john", age: 27
...> end
{:module, User,
<<70, 79, 82, ...>>, {:__struct__, 0}}
And then check:
iex> Blank.blank?(%{})
true
iex> Blank.blank?(%User{})
** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"}
Instead of sharing protocol implementation with maps, structs require their own protocol implementation:
defimpl Blank, for: User do
def blank?(_), do: false
end
If desired, you could come up with your own semantics for a user being
blank. Not only that, you could use structs to build more robust data
types, like queues, and implement all relevant protocols, such as
Enumerable
and possibly Blank
, for this data type.
In many cases though, developers may want to provide a default implementation for structs, as explicitly implementing the protocol for all structs can be tedious. That’s when falling back to Any comes in handy.
16.2 Falling back to Any¶
It may be convenient to provide a default implementation for all types.
This can be achieved by setting @fallback_to_any
to true
in the
protocol definition:
defprotocol Blank do
@fallback_to_any true
def blank?(data)
end
Which can now be implemented as:
defimpl Blank, for: Any do
def blank?(_), do: false
end
Now all data types (including structs) that we have not implemented the
Blank
protocol for will be considered non-blank.
16.3 Built-in protocols¶
Elixir ships with some built-in protocols. In previous chapters, we have
discussed the Enum
module which provides many functions that work
with any data structure that implements the Enumerable
protocol:
iex> Enum.map [1, 2, 3], fn(x) -> x * 2 end
[2,4,6]
iex> Enum.reduce 1..3, 0, fn(x, acc) -> x + acc end
6
Another useful example is the String.Chars
protocol, which specifies
how to convert a data structure with characters to a string. It’s
exposed via the to_string
function:
iex> to_string :hello
"hello"
Notice that string interpolation in Elixir calls the to_string
function:
iex> "age: #{25}"
"age: 25"
The snippet above only works because numbers implement the
String.Chars
protocol. Passing a tuple, for example, will lead to an
error:
iex> tuple = {1, 2, 3}
{1, 2, 3}
iex> "tuple: #{tuple}"
** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3}
When there is a need to “print” a more complex data structure, one can
simply use the inspect
function, based on the Inspect
protocol:
iex> "tuple: #{inspect tuple}"
"tuple: {1, 2, 3}"
The Inspect
protocol is the protocol used to transform any data
structure into a readable textual representation. This is what tools
like IEx use to print results:
iex> {1, 2, 3}
{1,2,3}
iex> %User{}
%User{name: "john", age: 27}
Keep in mind that, by convention, whenever the inspected value starts
with #
, it is representing a data structure in non-valid Elixir
syntax. This means the inspect protocol is not reversible as information
may be lost along the way:
iex> inspect &(&1+2)
"#Function<6.71889879/1 in :erl_eval.expr/5>"
There are other protocols in Elixir but this covers the most common ones.