Reading — step 1 of 7
Learn
Where protocols dispatch on data, behaviours dispatch on modules. A behaviour is an interface that modules can implement — Elixir's way of saying "any module that does X must have these callbacks."
Defining a behaviour
defmodule Cache do
@callback get(key :: String.t()) :: {:ok, term()} | :error
@callback put(key :: String.t(), value :: term()) :: :ok
@callback delete(key :: String.t()) :: :ok
end
The @callback attribute declares the contract — function name, parameter types, return type. No implementation here.
Implementing a behaviour
defmodule MemoryCache do
@behaviour Cache
use Agent
def start_link(_), do: Agent.start_link(fn -> %{} end, name: __MODULE__)
@impl true
def get(key) do
case Agent.get(__MODULE__, &Map.fetch(&1, key)) do
:error -> :error
{:ok, _} = ok -> ok
end
end
@impl true
def put(key, value) do
Agent.update(__MODULE__, &Map.put(&1, key, value))
end
@impl true
def delete(key) do
Agent.update(__MODULE__, &Map.delete(&1, key))
end
end
@behaviour Cache— declares that this module implements the Cache contract@impl true— marks each function as implementing a callback
The compiler verifies all @callbacks are implemented. Missing ones get warnings.
Built-in behaviours
Elixir/OTP has many:
GenServer— stateful server processesSupervisor— process supervision treesApplication— application lifecyclePlug— HTTP middleware (in the Plug library)Phoenix.LiveComponent— Phoenix LiveView components
Each defines a set of callbacks. Your module implements them; the framework calls them.
Optional callbacks
defmodule Cache do
@callback get(key :: String.t()) :: {:ok, term()} | :error
@callback put(key :: String.t(), value :: term()) :: :ok
@callback warmup() :: :ok
@optional_callbacks warmup: 0
end
With @optional_callbacks, modules can skip implementing those without warnings.
Behaviours vs protocols — when to use which
| Aspect | Protocol | Behaviour |
|---|---|---|
| Dispatch on | Value type | Module |
| Use case | Different data types respond to operations | Different modules implement a contract |
| Examples | Enumerable for List/Map/MyStruct | GenServer, Plug, Application |
| Defined with | defprotocol + defimpl | @callback in module + @behaviour in implementer |
Protocol example: Enum.map([1,2,3], f) — dispatches based on whether the first arg is a List, Map, etc.
Behaviour example: GenServer.start_link(MyServer, ...) — calls MyServer.init/1, MyServer.handle_call/3, etc., based on which module is passed.
Defining a custom behaviour
Useful for plugin systems, strategy patterns:
defmodule Notifier do
@callback send(message :: String.t()) :: :ok | {:error, term()}
end
defmodule EmailNotifier do
@behaviour Notifier
@impl true
def send(message), do: send_email(message)
end
defmodule SlackNotifier do
@behaviour Notifier
@impl true
def send(message), do: post_to_slack(message)
end
# Caller picks at runtime:
notifier = Application.get_env(:my_app, :notifier, EmailNotifier)
notifier.send("hello")
Classic strategy pattern. Configurable, testable, swappable.
Common mistakes
- Mixing protocol and behaviour — different mechanisms. Protocols dispatch on value; behaviours on module.
- Forgetting
@impl true— Elixir 1.5+ requires it for behaviour callbacks. Without, no warning when you mistype the function name. - Confusing @behavior (American) vs @behaviour (British) — Elixir uses BRITISH spelling (defbehaviour, @behaviour). American @behavior is silently wrong.
- Implementing only some callbacks — compiler warns. Either implement all or mark missing as @optional_callbacks.
- Behaviours where protocols would do — if you're dispatching on data, protocol. If on module, behaviour.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…