Behaviours

Many modules share the same public API. Take a look at Plug, which, as its description states, is a specification for composable modules in web applications. Each plug is a module which has to implement at least two public functions: init/1 and call/2.

Behaviours provide a way to:

  • define a set of functions that have to be implemented by a module;
  • ensure that a module implements all the functions in that set.

If you have to, you can think of behaviours like interfaces in object oriented languages like Java: a set of function signatures that a module has to implement.

Defining behaviours

Say we want to implement a bunch of parsers, each parsing structured data: for example, a JSON parser and a YAML parser. Each of these two parsers will behave the same way: both will provide a parse/1 function and an extensions/0 function. The parse/1 function will return an Elixir representation of the structured data, while the extensions/0 function will return a list of file extensions that can be used for each type of data (e.g., .json for JSON files).

We can create a Parser behaviour:

defmodule Parser do
  @callback parse(String.t) :: any
  @callback extensions() :: [String.t]
end

Modules adopting the Parser behaviour will have to implement all the functions defined with the @callback directive. As you can see, @callback expects a function name but also a function specification like the ones used with the @spec directive we saw above.

Adopting behaviours

Adopting a behaviour is straightforward:

defmodule JSONParser do
  @behaviour Parser

  def parse(str), do: # ... parse JSON
  def extensions, do: ["json"]
end
defmodule YAMLParser do
  @behaviour Parser

  def parse(str), do: # ... parse YAML
  def extensions, do: ["yml"]
end

If a module adopting a given behaviour doesn’t implement one of the callbacks required by that behaviour, a compile-time warning will be generated.