🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir Protocol Patterns

A protocol pattern is a reusable way to let different modules answer the same call in their own way. It fits when several types must answer one shared question.

Different structs answer one call in their own way

Book and Film are different structs. Each summary implementation uses its own label and title.

defmodule Book do
  defstruct title: ""
end

defmodule Film do
  defstruct title: ""
end

defprotocol Summarizable do
  def summary(value)
end

defimpl Summarizable, for: Book do
  def summary(%Book{title: title}), do: "Book: #{title}"
end

defimpl Summarizable, for: Film do
  def summary(%Film{title: title}), do: "Film: #{title}"
end

book = struct(Book, title: "Dune")
film = struct(Film, title: "Arrival")
IO.puts(Summarizable.summary(book))
IO.puts(Summarizable.summary(film))

One routine can summarise a mixed collection

Enum.map calls the protocol for each item. The collection can mix books and films.

defmodule Book do
  defstruct title: ""
end

defmodule Film do
  defstruct title: ""
end

defprotocol Summarizable do
  def summary(value)
end

defimpl Summarizable, for: Book do
  def summary(%Book{title: title}), do: "Book: #{title}"
end

defimpl Summarizable, for: Film do
  def summary(%Film{title: title}), do: "Film: #{title}"
end

book = struct(Book, title: "Dune")
film = struct(Film, title: "Arrival")
items = [book, film]
IO.puts(Enum.map(items, &Summarizable.summary/1) |> Enum.join(" | "))

Plain functions fit a single type

Doubling a number needs no protocol. One plain function handles the only type involved.

defmodule Single do
  def double(n), do: n * 2
end

IO.puts(Single.double(21))
IO.puts(Single.double(10))

Example

defmodule Book do
  defstruct title: ""
end

defmodule Film do
  defstruct title: ""
end

defprotocol Summarizable do
  def summary(value)
end

defimpl Summarizable, for: Book do
  def summary(%Book{title: title}), do: "Book: #{title}"
end

defimpl Summarizable, for: Film do
  def summary(%Film{title: title}), do: "Film: #{title}"
end

defmodule Single do
  def double(n), do: n * 2
end

book = struct(Book, title: "Dune")
film = struct(Film, title: "Arrival")
items = [book, film]
IO.puts(Enum.map(items, &Summarizable.summary/1) |> Enum.join(" | "))
IO.puts(Single.double(21))

Key points

  • Polymorphism: One protocol call can run different module code depending on the value's type.
  • Shared question: Protocols work best when every implementation answers the same question, like a summary or a size.
  • Composition: A larger routine can call a protocol for each item in a mixed collection.
  • Behaviour choice: A behaviour fits better when the caller picks a module by name, not by a value's type.
  • Plain functions: Plain functions fit better when only one type is involved and the choice never changes.

In this example, the Summarizable protocol is answered differently by Book and by Film. Then one routine maps the protocol over a mixed collection, which is the composition pattern. Finally a plain function doubles a number, showing the single-type case where no protocol is needed.

Note

A protocol for a single type adds indirection without a second implementation to justify it.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A caller always picks a module by name, and values never decide the code. Which fits best?