🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir Protocol Definitions

A protocol is a set of function promises that different data types can keep in their own way. It lets one function name work on many kinds of data.

A protocol lists several promised functions

The Sized protocol promises size, emptiness, and presence. The list implementation keeps all three promises.

defprotocol Sized do
  def size(data)
  def empty?(data)
  def present?(data)
end

defimpl Sized, for: List do
  def size(list), do: length(list)
  def empty?(list), do: list == []
  def present?(list), do: list != []
end

IO.puts(Sized.size([10, 20, 30]))
IO.puts(Sized.empty?([]))
IO.puts(Sized.present?([10]))

Dispatch picks the implementation for the value's type

The same size call runs different code for a string and a list. Each type counts in its own way.

defprotocol Sized do
  def size(data)
  def empty?(data)
  def present?(data)
end

defimpl Sized, for: List do
  def size(list), do: length(list)
  def empty?(list), do: list == []
  def present?(list), do: list != []
end

defimpl Sized, for: BitString do
  def size(string), do: String.length(string)
  def empty?(string), do: string == ""
  def present?(string), do: string != ""
end

IO.puts(Sized.size("hello"))
IO.puts(Sized.size([10, 20]))
IO.puts(Sized.empty?(""))
IO.puts(Sized.present?("hi"))

A new type gets its own implementation

A tuple implementation counts with tuple_size. The protocol call stays the same while the code changes.

defprotocol Sized do
  def size(data)
  def empty?(data)
  def present?(data)
end

defimpl Sized, for: Tuple do
  def size(tuple), do: tuple_size(tuple)
  def empty?(tuple), do: tuple_size(tuple) == 0
  def present?(tuple), do: tuple_size(tuple) != 0
end

IO.puts(Sized.size({10, 20, 30}))
IO.puts(Sized.empty?({}))
IO.puts(Sized.present?({1}))

Example

defprotocol Sized do
  def size(data)
  def empty?(data)
  def present?(data)
end

defimpl Sized, for: List do
  def size(list), do: length(list)
  def empty?(list), do: list == []
  def present?(list), do: list != []
end

defimpl Sized, for: BitString do
  def size(string), do: String.length(string)
  def empty?(string), do: string == ""
  def present?(string), do: string != ""
end

defimpl Sized, for: Tuple do
  def size(tuple), do: tuple_size(tuple)
  def empty?(tuple), do: tuple_size(tuple) == 0
  def present?(tuple), do: tuple_size(tuple) != 0
end

IO.puts(Sized.size("hello"))
IO.puts(Sized.size([10, 20, 30]))
IO.puts(Sized.size({10, 20}))
IO.puts(Sized.empty?([]))
IO.puts(Sized.present?("hi"))

Key points

  • Protocol promise: A protocol lists function names and arities without giving their bodies.
  • Dispatch: Calling a protocol function picks the implementation for the value's type.
  • One name, many types: The same call can work for a string, a list, and a tuple when each has an implementation.
  • Definition shape: defprotocol names the protocol and def lists each promised function.
  • Several functions: One protocol can promise several functions, and each implementation covers all of them.

In this example, the Sized protocol promises size, empty?, and present?. Then a list implementation, a string implementation, and a tuple implementation each keep those promises in their own way. Finally the same calls run for a string, a list, and a tuple, and each result comes from the matching implementation.

Note

Without an implementation for a value's type, the call raises an error.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

Which implementation runs when Sized.size is called with a string?