Defines a protocol.
A protocol specifies an API that should be defined by its
implementations.
Examples
In Elixir, only false
and nil
are considered falsy values.
Everything else evaluates to true in if
clauses. 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 implement this protocol as follow:
defprotocol Blank do
@doc "Returns true if data is considered blank/empty"
def blank?(data)
end
Now that the protocol is defined, we can implement it. We need
to implement the protocol for each Elixir type. For example:
# Integers are never blank
defimpl Blank, for: Integer do
def blank?(number), do: false
end
# Just empty list is blank
defimpl Blank, for: List do
def blank?([]), do: true
def blank?(_), do: false
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 have to define the implementation for all types.
The supported types available are:
Protocols + Structs
The real benefit of protocols comes when mixed with structs.
For instance, Elixir ships with many data types implemented as
structs, like HashDict
and HashSet
. We can implement the
Blank
protocol for those types as well:
defimpl Blank, for: [HashDict, HashSet] do
def blank?(enum_like), do: Enum.empty?(enum_like)
end
If a protocol is not found for a given type, it will fallback to
Any
.
Fallback to any
In some cases, 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: true
end
One may wonder why such fallback is not true by default.
It is two-fold: first, the majority of protocols cannot
implement an action in a generic way for all types. In fact,
providing a default implementation may be harmful, because users
may rely on the default implementation instead of providing a
specialized one.
Second, falling back to Any
adds an extra lookup to all types,
which is unnecessary overhead unless an implementation for Any is
required.
Types
Defining a protocol automatically defines a type named t
, which
can be used as:
@spec present?(Blank.t) :: boolean
def present?(blank) do
not Blank.blank?(blank)
end
The @spec
above expresses that all types allowed to implement the
given protocol are valid argument types for the given function.
Reflection
Any protocol module contains three extra functions:
__protocol__/1
- returns the protocol name when :name
is given, and a
keyword list with the protocol functions when :functions
is given
impl_for/1
- receives a structure and returns the module that
implements the protocol for the structure, nil
otherwise
impl_for!/1
- same as above but raises an error if an implementation is
not found
Consolidation
In order to cope with code loading in development, protocols in
Elixir provide a slow implementation of protocol dispatching specific
to development.
In order to speed up dispatching in production environments, where
all implementations are known up-front, Elixir provides a feature
called protocol consolidation. For this reason, all protocols are
compiled with debug_info
set to true, regardless of the option
set by elixirc
compiler. The debug info though may be removed
after consolidation.
For more information on how to apply protocol consolidation to
a given project, please check the functions in the Protocol
module or the mix compile.protocols
task.