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?