Behaviours

An Elixir behaviour is nothing more than a list of functions. A module that declares that it implements a particular behaviour must implement all of the associated functions. If it doesn’t, Elixir will generate a compilation warning. You can think of a behaviour definition as being like an abstract base class in some object-oriented languages.

A behaviour is therefore a little like an interface in Java. A module uses it to declare that it implements a particular interface. For example, an OTP GenServer should implement a standard set of callbacks (handle_call, handle_cast, and so on). By declaring that our module implements that behaviour, we let the compiler validate that we have actually supplied the necessary interface. This reduces the chance of an unexpected runtime error.

Defining Behaviours

We define a behaviour with @callback definitions.

For example, the mix utility can fetch dependencies from various source-code control systems. Out of the box, it supports git and the local filesystem. However, the interface to the source-code control system (which mix abbreviates internally as SCM) is defined using a behaviour, allowing new version-control systems to be added cleanly.

The behaviour is defined in the module Mix.Scm:

 defmodule​ Mix.SCM ​do
  @moduledoc ​"""
  This module provides helper functions and defines the behaviour
  required by any SCM used by Mix.
  """
 
  @type opts :: Keyword.t
 
  @doc ​"""
  Returns a boolean if the dependency can be fetched or it is meant to
  be previously available in the filesystem.
 
  Local dependencies (i.e. non fetchable ones) are automatically
  recompiled every time the parent project is compiled.
  """
  @callback fetchable? :: boolean
 
  @doc ​"""
  Returns a string representing the SCM. This is used when printing
  the dependency and not for inspection, so the amount of information
  should be concise and easy to spot.
  """
  @callback format(opts) :: String.t
 
 # and so on for 8 more callbacks

This module defines the interface that modules implementing the behaviour must support. It uses @callback to define the functions in the behaviour. But the syntax looks a little different. That’s because we’re using a minilanguage: Erlang type specifications. For example, the fetchable? function takes no parameters and returns a Boolean. The format function takes a parameter of type opts (which is defined near the top of the code to be a keyword list) and returns a string. There’s more information on these type specifications here.

In addition to the type specification, we can include module- and function-level documentation with our behaviour definitions.

Declaring Behaviours

Now that we’ve defined the behaviour, we can declare that another module implements it using the @behaviour attribute. Here’s the start of the Git implementation for mix:

 defmodule​ Mix.SCM.Git ​do
  @behaviour Mix.SCM
 
 def​ fetchable? ​do
  true
 end
 
 def​ format(opts) ​do
  opts[​:git​]
 end
 
 # . . .
 end

The module defines each of the functions declared as callbacks in Mix.SCM. This module will compile cleanly. However, imagine we’d misspelled fetchable:

 defmodule​ Mix.SCM.Git ​do
  @behaviour Mix.SCM
 
»def​ fetchible? ​do
  true
 end
 
 def​ format(opts) ​do
  opts[​:git​]
 end
 
 # . . .
 end

When we compile the module, we’d get this error:

 git.ex:1: warning: undefined behaviour function fetchable?/0 (for behaviour Mix.SCM)

Behaviours give us a way of both documenting and enforcing the public functions that a module should implement.

Taking It Further

In the implementation of Mix.SCM for Git, we created a bunch of functions that implemented the behaviour. But those are unlikely to be the only functions in this module. And, unless you’re intimately familiar with the Mix.SCM behaviour, you won’t be able to tell the callback functions from the rest.

To remedy this, you can flag the callback functions with the @impl attribute. This takes a parameter: either true or the name of a behaviour (guess which one I prefer).

 defmodule​ Mix.SCM.Git ​do
  @behaviour Mix.SCM
 
 def​ init(arg) ​do​ ​# plain old function
 # ...
 end
 
  @impl Mix.SCM ​# callback
 def​ fetchable? ​do
  true
 end
 
  @impl Mix.SCM ​# callback
 def​ format(opts) ​do
  opts[​:git​]
 end