I-Logs blog

i-logs SPRL

Erlang User defined types

published by Grégoire Welraeds, managing partner at i-logs on 26/03/2018

Reading time: 8 minutes

This post was initially published on medium.com.

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:

Why you should use custom type?

  1. to introduce abstraction in you programs;
  2. 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 

  1. to document function interfaces;
  2. to provide more information for bug detection tools, such as Dialyzer
  3. 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:


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

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.

-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 Cards custom types in the generated edoc

the kind() function specification 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:

  1. Type and Function specifications from the official Erlang documentation;
  2. Types (or lack thereof) from learn you some Erlang.
  3. Dialyzer from the official Erlang documentation
  4. 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.