🎓 A1Learner
🔍
0 of 46 lessons completed

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?