Protocols
Learn about protocols and their usage in Elixir.
We'll cover the following
Introduction
We’ve used the inspect
function many times in this course. It returns a printable representation of any value as a binary, which is what we call strings.
But let’s stop and think for a minute. Just how can Elixir, which doesn’t have objects, know what to call to do the conversion to a binary? We can pass inspect
anything, and Elixir somehow makes sense of it. It could be done using guard clauses:
def inspect(value) when is_atom(value), do: ...
def inspect(value) when is_binary(value), do: ...
: :
But there’s a better way.
Elixir has the concept of protocols. A protocol is a little like the behaviours we saw in the previous chapter in that it defines the functions that must be provided to achieve something. But a behaviour is internal to a module; the module implements the behaviour. Protocols are different because we can place a protocol’s implementation completely outside the module. This means we can extend modules’ functionality without having to add code to them. In fact, we can extend the functionality even if we don’t have the modules’ source code.
Defining a protocol
Protocol definitions are very similar to basic module definitions. They can contain module- and function-level documentation (@moduledoc
and @doc
), and they’ll contain one or more function definitions. However, these functions won’t have bodies. Their job is simply to declare the interface that the protocol requires.
For example, here’s the definition of the Inspect
protocol:
defprotocol Inspect do
@fallback_to_any true
def inspect(thing, opts)
end
Just like a module, the protocol defines one or more functions. But we implement the code separately.
Implementing a protocol
The defimpl
macro lets us give Elixir the implementation of a protocol for one or more types. The code that follows is the implementation of the Inspect
protocol for PIDs and references.
defimpl Inspect, for: PID do
def inspect(pid, _opts) do
"#PID" <>IO.iodata_to_binary(pid_to_list(pid))
end
end
defimpl Inspect, for: Reference do
def inspect(ref, _opts) do
'#Ref' ++ rest = :erlang.ref_to_list(ref)
"#Reference" <> IO.iodata_to_binary(rest)
end
end
Finally, the Kernel
module implements inspect
, which calls Inspect.inspect
with its parameter. This means that when we call inspect(self)
, it becomes a call to Inspect.inspect(self)
. And because self
is a PID, this in turn resolves to something like "#PID<0.25.0>"
.
Behind the scenes, defimpl
puts the implementation for each protocol-and-type combination into a separate module. The protocol for Inspect
for the PID type is in the module Inspect.PID
. Because we can recompile modules, we can change the implementation of functions accessed via protocols.
iex> inspect self
"#PID<0.25.0>"
iex> defimpl Inspect, for: PID do
...> def inspect(pid, _) do
...> "#Process: " <> IO.iodata_to_binary(:erlang.pid_to_list(pid)) <> "!!"
...> end
...> end
iex:3: redefining module Inspect.PID
{:module, Inspect.PID, <<70,79....
iex> inspect self
"#Process: <0.25.0>!!"
Try running the above commands below:
Get hands-on with 1400+ tech skills courses.