This story is cross-posted from my Collected Notes.
Introduction
This is just a quick note to myself about Erlang custom types, just because I wanted to define my own types and use them in a Erlang script but could not figured out how to do it, even after reading a lot of documentation. In the end, I was wrong about what custom types are in Erlang.
I have started learning Erlang recently. This article is written from a total Erlang newbie perspective, so please excuse any approximation. Do not hesitate to comment.
In a Nutshell
What is important to know when using custom types in Erlang:
- Erlang is a dynamically typed language: there’s no static type checker in the compiler, every error is caught at runtime (or not) and the compiler won’t almost never yell at you when compiling code for type reasons.
- there have been some attempts to build type systems on top of Erlang, the most notable one happened back in 1997 and the results were somewhat disappointing (only a subset of the language was type-checkable);
-
-type
and-spec
are only annotation and could not be used to introduce some sort of static type checking - use custom types and specifications mainly for documentation purposes (along with edoc tags);
- use typer annotation tool to help you with the specification definitions;
- Use Dializer tool once in while to perform a static analysis of your code and identifies software discrepancies such as definite type errors.
Why you should use custom type?
- to introduce abstraction in you programs;
- to document your program
Programs of a certain size are complex. And as the program grows, so goes the complexity. By using abstraction and a clean specification, you can make your program more readable for you and for your fellow programmers who need to understand your code.
Please note once again that this abstraction is not enforced by the use of the erlang compiler. Nevertheless, dializer provide us with much of the necessary tooling for abstraction.
As far as the documentation is concerned, custom types are used
- to document function interfaces;
- to provide more information for bug detection tools, such as Dialyzer
- To be exploited by documentation tools, such as EDoc, for generating program documentation of various forms
How to define a custom type?
First, you need to understand what an erlang type is. The basic syntax of a type is an atom followed by closed parentheses. New types are declared using -type
and -opaque
attributes as in the following:
-module(cards1).
-type suite() :: spades | clubs | hearts | diamonds.
-type value() :: 1..10 | j | q | k.
Types declared as opaque represent sets of terms whose structure is not supposed to be visible from outside of their defining module. That is, only the module defining them is allowed to depend on their term structure. Consequently, such types do not make much sense as module local and should always to be exported.
Please note that if we are trying to compile the above code, the compiler will warns us that suite and value types are unused (which is true).
Where to use a custom type?
Four different places:
1. In custom types definition
Starting from the example above, we have added the card
type which is a tuple made of one suite
and one value
.
-module(cards).
-type suite() :: spades | clubs | hearts | diamonds.
-type value() :: 1..10 | j | q | k.
-type card() :: {suit(), value()}.
2. In other modules
This one is an extension of the previous example. You can export locally defined types using the export
type compiler directive. In the example below, we export suite()
and value()
type from the suite
module...
-module(suite).
-export_type([suite/0,value/0]).
-type suite() :: spades | clubs | hearts | diamonds.
-type value() :: 1..10 | j | q | k.
... and use it in the card
module:
-module(cards).
-export([kind/1, main/0]).
-type card() :: {suite:suite(), suite:value()}.
3. In record definition
It is possible to use custom types to specify the type of records fields like in the example below. We define the type hand
as being a list of cards
and create a new player
record which has an id and a hand
.
-module(cards).
-export([kind/1, main/0]).
-type card() :: {suite:suite(), suite:value()}.
-type hand() :: [card()]. % hand is a list of cards.
-record(player, {id :: integer(), % player id
hand = [] :: hand() % player hand
}).
4. In specification
As stated before, specifications are available in Erlang to document a function interface. A specification (or contract) for a function is given using the -spec
attribute. We are not digging the Erlang function specification system here (might be for another post), let’s go straight to an example. Below, the kind()
method returns either the atom number
when the value of a card is a number or the atom face
when the value is either j, q or k. Thanks to our custom types, we can introduce some abstraction by indicating the kind method as a single argument of type card.
-module(cards).
-type suite() :: spades | clubs | hearts | diamonds.
-type value() :: 1..10 | j | q | k.
-type card() :: {suit(), value()}.
% using card custom type in our function specification
-spec kind(card()) -> face | number.
kind({_, A}) when A >= 1, A =< 10 -> number;
kind(_) -> face.
Use Dialyzer!
As already stated before, the use of custom types is not enforced by the Erlang compiler.
Below is the full code of our cards module. Not very useful, I must say, but the idea is to demonstrate the use of Dialyzer.
-module(cards).
-export([kind/1, main/0]).
-type suite() :: spades | clubs | hearts | diamonds.
-type value() :: 1..10 | j | q | k.
-type card() :: {suite(), value()}.
-spec kind(card()) -> face | number.
kind({_, A}) when A >= 1, A =< 10 -> number;
kind(_) -> face.
%% @return atom
main() ->
number = kind({spades, 7}),
face = kind({hearts, k}),
%% The line below is not compatible with our contract
%% There is no rubies in the suite() atom list.
number = kind({rubies, 4}),
face = kind({clubs, q})
If you pay attention to the code, you will notice the line 19: number = kind({rubies, 4})
Obviously, this code does not respect the specification of the kind()
function (line 8) because a suite()
can only be a spades
, clubs
, hearts
or diamonds
. There is no rubies in that list of atom.
Try to compile this code, no warning will be raised. Execute this code using erlang repl, same same: the {rubies, 4}
pattern match the function declaration kind({_,A})
, it is ok for Erlang which does not care about your type and specs at all.
Let’s call the dialyzer tool to the rescue. Dialyzer is a static analysis tool that identifies software discrepancies, such as definite type errors, code that has become dead or unreachable because of programming error, and unnecessary tests, in single Erlang modules or entire (sets of) applications.
I’ll let you read the documentation about how to run the dializer tool on your code. When we run the dialyzer tool on our cards.erl file, the following line will be emitted during the analysis:
cards.erl:A9: The call cards:kind({‘rubies’,4}) breaks the contract (card()) -> ‘face’ | ‘number
As you can see, Dialyzer uses the type information provided and is able to detect the lack of commitment to the kind() contract. And to quote (4) (see below), Dialyzer will not catch everything, but when it does complain, it’s guaranteed to be an error.
Generate the doc
As stated in the beginning of this post, custom types are useful for abstraction AND documentation. When generating the documentation for your application using the Erlang edoc module, the type definition and functions specifications will be taken into consideration. This is a really nice feature which you can use to document your code for large application, when working as part as a team or if you have to maintain the code in a long period of time.
The two images below shows data types and functions specifications for the cards module we defined before:
Cards custom types in the generated edoc
the kind() function specification in the generated edoc
Lets recap
Keep in mind that custom types in erlang are mainly used for creating a higher level of abstraction and for documentation purpose. They are mainly useful if you run the dialyzer tool regularly, in your build process for instance. They are also useful when the application documentation is generated using the edoc specification and the rebar tool.
Further Reading:
- Type and Function specifications from the official Erlang documentation;
- Types (or lack thereof) from learn you some Erlang.
- Dialyzer from the official Erlang documentation
- Getting started with dialyzer for Erlang
Full disclosure, the whole card/suite/… examples provided in this post are coming from this page. No copyrights infringement intended, just though it was a good example.
Top comments (0)