A practical example of Elixir behaviours

September 20, 2019

Assuming you know the theory, this example will show how different implementations of Behaviour will be called depending on an environment you’re on.


Let’s start a new project:

mix new demobehaviour --sup


Behaviours

Create a new file formatter.ex in lib/demobehaviour directory:

# lib/demobehaviour/formatter.ex

defmodule Demobehaviour.Formatter do
  @callback format(String.t()) :: {:ok, term} | {:error, String.t()}
end


Create another new file default_formatter.ex in lib/demobehaviour directory:

# lib/demobehaviour/default_formatter.ex

defmodule Demobehaviour.DefaultFormatter do
  @behaviour Demobehaviour.Formatter

  @impl true
  def format(str) do
    {:ok, "default format: " <> str}
  end
end


And the last implementation, a new file custom_formatter.ex in lib/demobehaviour directory:

# lib/demobehaviour/custom_formatter.ex

defmodule Demobehaviour.CustomFormatter do
  @behaviour Demobehaviour.Formatter

  @impl true
  def format(str) do
    {:ok, "custom format: " <> str}
  end
end


For sake of simplicity we just display a different string for each implemenation. The point of this example is to show how to combine behaviours with configuration.

Add a new directory config to the root of the project and put there a new file config.exs

Config

# config/config.exs

import Config

config :demobehaviour, :formatter, Demobehaviour.CustomFormatter

import_config "#{Mix.env()}.exs"


We basicaly state here that :formatter will point to our default formatter and we’ll import configuratiom based on Mix.env().

For our Dev environment, we’ll create dev.exs in config directory:

# config/dev.exs

import Config

config :demobehaviour, :formatter, Demobehaviour.CustomFormatter


Which basically sets :formatter to our custom one.


And for our Prod environment, we’ll create prod.exs in config directory:

# config/prod.exs

import Config

config :demobehaviour, :formatter, Demobehaviour.DefaultFormatter


Which sets :formatter to the default one.


Usage

Let’s put that together. Replace content of lib/demobehaviour.ex with:

# lib/demobehaviour.ex

defmodule Demobehaviour do

  def format(str) do
    formatter = Application.get_env(:demobehaviour, :formatter)
    formatter.format(str)
  end
end


Test

Prod env:

$ MIX_ENV=prod mix run -e 'IO.inspect(Demobehaviour.format("should be default"))'
{:ok, "default format: should be default"}


Dev env:

$ MIX_ENV=dev mix run -e 'IO.inspect(Demobehaviour.format("should be custom"))'
{:ok, "custom format: should be custom"}


As you see we don’t change the call Demobehaviour.format and we’re getting different results for different environments.


Bonus part:

Although this post wasn’t about Behaviours vs Protocols, I bet you asked yourself this question at some point.

If you’re still wondering when to use which, I recommend you watching Kevin Rockwood | A Practical Guide to Elixir Protocols” on youtube. As Kevin says: “The rule of thumb there if your functions can take the same data type, user a behaviour. If you need your functions to take different data for each implementation than lean towards a protocol”