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.
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.
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.
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 |