20 Typespecs and behaviours¶
{% include toc.html %}
20.1 Types and specs¶
Elixir is a dynamically typed language, so all types in Elixir are inferred by the runtime. Nonetheless, Elixir comes with typespecs, which are a notation used for:
- declaring custom data types;
- declaring typed function signatures (specifications).
20.1.1 Function specifications¶
By default, Elixir provides some basic types, such as integer
or
pid
, as well as more complex types: for example, the round/1
function, which rounds a float to its nearest integer, takes a
number
as an argument (an integer
or a float
) and returns an
integer
. As you can see in its
documentation,
round/1
‘s typed signature is written as:
round(number) :: integer
::
means that the function on the left side returns a value whose
type is what’s on the right side. Function specs are written with the
@spec
directive, placed right before the function definition. The
round/1
function could be written as:
@spec round(number) :: integer
def round(number), do: # implementation...
Elixir supports compound types as well. For example, a list of integers
has type [integer]
. You can see all the types provided by Elixir in
the typespecs
docs.
20.1.2 Defining custom types¶
While Elixir provides a lot of useful built-in types, it’s convenient to
define custom types when appropriate. This can be done when defining
modules modules through the @type
directive.
Say we have a LousyCalculator
module, which performs the usual
arithmetic operations (sum, product and so on) but, instead of returning
numbers, it returns tuples with the result of an operation as the first
element and a random offense as the second element.
defmodule LousyCalculator do
@spec add(number, number) :: {number, String.t}
def add(x, y), do: {x + y, "You need a calculator to do that?!"}
@spec multiply(number, number) :: {number, String.t}
def multiply(x, y), do: {x * y, "Jeez, come on!"}
end
As you can see in the example, tuples are a compound type and each tuple
is identified by the types inside it. To understand why String.t
is
not written as string
, have another look at the typespecs
docs.
Defining function specs this way works, but it quickly becomes annoying
since we’re repeating the type {number, String.t}
over and over. We
can use the @type
directory in order to declare our own custom type.
defmodule LousyCalculator do
@typedoc """
Just a number followed by a string.
"""
@type number_with_offense :: {number, String.t}
@spec add(number, number) :: number_with_offense
def add(x, y), do: {x + y, "You need a calculator to do that?!"}
@spec multiply(number, number) :: number_with_offense
def multiply(x, y), do: {x * y, "Jeez, come on!"}
end
The @typedoc
directive, similarly to the @doc
and @moduledoc
directives, is used to document custom types.
Custom types defined through @type
are exported and available
outside the module they’re defined in:
defmodule PoliteCalculator do
@spec add(number, number) :: number
def add(x, y), do: make_polite(LousyCalculator.add(x, y))
@spec make_polite(LousyCalculator.number_with_offense) :: number
defp make_polite({num, _offense}), do: num
end
If you want to keep a custom type private, you can use the @typep
directive instead of @type
.
20.1.3 Static code analysis¶
Typespecs are not only useful to developers and as additional
documentation. The Erlang tool
Dialyzer, for example,
uses typespecs in order to perform static analysis of code. That’s why,
in the PoliteCalculator
example, we wrote a spec for the
make_polite/1
function even if it was defined as a private function.
20.2 Behaviours¶
Many modules share the same public API. Take a look at
Plug, which, as it 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
.
Behaviors 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.
20.2.1 Defining behaviours¶
Say we have 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 a 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
use Behaviour
defcallback parse(String.t) :: any
defcallback extensions() :: [String.t]
end
Modules adopting the Parser
behaviour will have to implement all the
functions defined with defcallback
. As you can see, defcallback
expects a function name but also a function specification like the ones
used with the @spec
directive we saw above.
20.2.2 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.