Elixir Behaviours in Depth
A behaviour is a contract that names the functions a module must provide. It lets different modules share one interface while doing different work.
A behaviour names its required functions
The Store behaviour needs two functions. One reads a value and one writes a value.
defmodule Store do
@callback fetch(term()) :: {:ok, term()} | :error
@callback store(term(), term()) :: :ok
end
IO.inspect({:fetch, 1} in Store.behaviour_info(:callbacks))
IO.inspect({:store, 2} in Store.behaviour_info(:callbacks))
IO.inspect(length(Store.behaviour_info(:callbacks)))An implementing module provides every callback
MemoryStore follows Store. It answers a known key and reports an unknown key as missing.
defmodule Store do
@callback fetch(term()) :: {:ok, term()} | :error
@callback store(term(), term()) :: :ok
end
defmodule MemoryStore do
@behaviour Store
def fetch(:a), do: {:ok, 1}
def fetch(_key), do: :error
def store(_key, _value), do: :ok
end
IO.inspect(MemoryStore.fetch(:a))
IO.inspect(MemoryStore.fetch(:b))
IO.inspect(MemoryStore.store(:b, 2))One caller works with any implementing module
A function can take a module as an argument. It calls the same callback on each module it receives.
defmodule Store do
@callback fetch(term()) :: {:ok, term()} | :error
@callback store(term(), term()) :: :ok
end
defmodule MemoryStore do
@behaviour Store
def fetch(:a), do: {:ok, 1}
def fetch(_key), do: :error
def store(_key, _value), do: :ok
end
defmodule FixedStore do
@behaviour Store
def fetch(_key), do: {:ok, 99}
def store(_key, _value), do: :ok
end
defmodule Reader do
def read(store, key), do: store.fetch(key)
end
IO.inspect(Reader.read(MemoryStore, :a))
IO.inspect(Reader.read(FixedStore, :a))
IO.inspect(Reader.read(FixedStore, :missing))Example
defmodule Store do
@callback fetch(term()) :: {:ok, term()} | :error
@callback store(term(), term()) :: :ok
end
defmodule MemoryStore do
@behaviour Store
def fetch(:a), do: {:ok, 1}
def fetch(_key), do: :error
def store(_key, _value), do: :ok
end
defmodule EchoStore do
@behaviour Store
def fetch(_key), do: {:ok, 7}
def store(_key, _value), do: :ok
end
defmodule UseStore do
def get_or_default(store, key, default) do
case store.fetch(key) do
{:ok, value} -> value
:error -> default
end
end
end
IO.inspect(UseStore.get_or_default(MemoryStore, :a, 0))
IO.inspect(UseStore.get_or_default(MemoryStore, :z, 0))
IO.inspect(UseStore.get_or_default(EchoStore, :z, 0))Key points
- Contract: a behaviour lists function names and arities that an implementing module must define.
- Declaration: a module declares a behaviour with the @behaviour attribute naming the behaviour module.
- Callback: a @callback attribute states one required function and its argument and return types.
- Dispatch: calling through a module variable lets one caller work with any module that follows the behaviour.
- Inspection: behaviour_info on a behaviour module returns its callback list.
In this example, Store names fetch and store, MemoryStore and EchoStore each provide both, and UseStore turns a fetch result into a value or a default. The same helper gives 1 for a known key, 0 for a missing key in MemoryStore, and 7 from EchoStore.
Note
a module that declares a behaviour but misses a callback gets a compile warning.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Which line declares that MemoryStore follows the Store behaviour?