Elixir Cheatsheet

Protocols and Behaviours

Use this Elixir reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Protocols: Polymorphism by Data Type

defprotocol Size do
  @doc "Number of elements in the data structure."
  def size(data)
end

defimpl Size, for: BitString do
  def size(binary), do: byte_size(binary)
end

defimpl Size, for: Map do
  def size(map), do: map_size(map)
end

defimpl Size, for: Tuple do
  def size(tuple), do: tuple_size(tuple)
end

Size.size("abc")        # 3 — dispatches on the value's type
Size.size(%{a: 1})      # 1

A protocol dispatches to the implementation for the first argument's data type. Built-in dispatch types: Atom, BitString, Float, Function, Integer, List, Map, PID, Port, Reference, Tuple, Any, and any struct module.

Implementing for Structs

defmodule Cart do
  defstruct items: []
end

defimpl Size, for: Cart do
  def size(%Cart{items: items}), do: length(items)
end

# fallback for everything else
defprotocol Size do
  @fallback_to_any true
  def size(data)
end

defimpl Size, for: Any do
  def size(_), do: 0
end

defmodule Order do
  @derive [Size]              # use the Any implementation for this struct
  defstruct items: []
end

The Protocols You Meet Daily

Enum.map(my_struct, & &1)     # Enumerable — implement to make a type Enum-able
to_string(value)              # String.Chars — powers "#{value}" interpolation
inspect(value)                # Inspect — controls IEx/log rendering
Enum.into(pairs, %{})         # Collectable — build a container from elements
Jason.encode!(struct)         # Jason.Encoder — @derive {Jason.Encoder, only: [...]}
defimpl String.Chars, for: Money do
  def to_string(%Money{cents: c}), do: "$#{c / 100}"
end

defmodule User do
  @derive {Inspect, except: [:password_hash]}   # redact fields in logs
  defstruct [:email, :password_hash]
end

Behaviours: Contracts for Modules

defmodule Parser do
  @callback parse(String.t()) :: {:ok, term()} | {:error, atom()}
  @callback extensions() :: [String.t()]
  @optional_callbacks extensions: 0
end

defmodule JSONParser do
  @behaviour Parser

  @impl Parser
  def parse(str), do: Jason.decode(str)

  @impl Parser
  def extensions, do: ["json"]
end

A behaviour is a set of @callback typespecs a module promises to implement. @behaviour opts in; @impl marks each callback (the compiler warns on typos and missing callbacks). GenServer, Supervisor, Application, and Plug are all behaviours.

Protocols vs Behaviours

ProtocolBehaviour
Dispatches onData type of a valueModule name you pass around
Define withdefprotocol / defimpl@callback + @behaviour
Typical useEnumerable, Inspect, JSON encodingGenServer, pluggable adapters
# behaviour-style plug-in module, configured then called by name
parser = Application.fetch_env!(:my_app, :parser)   # e.g. JSONParser
parser.parse(input)

Swappable Implementations in Config

# config/config.exs
config :my_app, :mailer, MyApp.Mailer.SMTP
# config/test.exs
config :my_app, :mailer, MyApp.Mailer.Fake

defp mailer, do: Application.fetch_env!(:my_app, :mailer)
def notify(user), do: mailer().send(user.email, "Welcome")

Behaviour + config-selected module is the standard pattern for test fakes and adapters (the approach libraries like Mox formalize: Mox.defmock(MailerMock, for: MyApp.Mailer)).