Programmers coming from a procedural background and trying to approach functional languages for the first time are usually told to forget everything they’ve learned and approach functional programming from a tabula rasa state of mind. This is a lot like Douglas Adams’s advice on how to fly being “The knack lies in learning how to throw yourself at the ground and miss” - they’re both concise, poetic, accurate, and do very little to actually help you achieve your stated goal.
In this series I’ll try a different approach - I’ll start by plugging a square peg into a round hole and just writing Erlang code in a procedural-like style, then move away from that to more Erlang-like code. Let’s start with a “Hello world!” one-liner and not a tail-recursive factorial or anything like that:
start() -> io:format("Hello world!~n").
Here we’re simply defining the function start
taking zero arguments (the empty parentheses) as a single statement: a call to the io
module’s format()
function with the sole argument being a string. Tilde in Erlang is the escaping character, replacing the backslash in C, so ~n
is a newline. The period signifies the last statement of a function.
To actually run this, we’ll need to create a module and compile it. I’ll be assuming you figured out how to install erlang and run erl
, the Erlang shell, from a terminal. Let’s create the file hello.erl
with the following contents:
% Hello Erlang module
-module(hello).
-export([start/0]).
start() -> io:format("Hello world!~n").
The %
indicates a comment. The two folowing statements declare a module, and declare what functions this module exports. Let’s decrypt the export statement - export takes a list of functions to export, so the empty export statement looks like -export([]).
and can be omitted. Erlang overloading relies only on the number of arguments a function takes (its arity), and since we’re only exporting one function taking zero arguments the list in our export statement is [start/0]
. If we had other functions with other argument counts we could have our list look like [foo/0, bar/1, baz/2]
- and we’ll get to that eventually.
Now that we have a module, let’s launch the erlang shell erl
from the same directory as hello.erl
and compile our module using the c/1
function. We’ll call it as c(hello).
then hit return. The output should look like this:
Eshell V10.4.1 (abort with ^G)
1> c(hello).
{ok,hello}
The {ok,hello}
is the result of evaluating c
. It’s a tuple, with the first element indicating success. To actually call our start function from the Erlang shell we’ll use the following syntax module_name:function_name(<arguments>).
:
2> hello:start().
Hello world!
ok
Now let’s add and print a variable. Variables in Erlang start with an uppercase letter, so we’ll modify start
to take a variable, print it, and update our export statement accordingly. Here’s what the new code looks like:
%Comments come after a % sign
-module(hello).
-export([start/1]).
start(X) -> io:format("X is ~w!~n", [X]).
Note the use of ~w
instead of the various format specifiers, and the addition of a list argument to io:format
containing the item(s) to print. Here, X
maps to the first (and only) instance of ~w
.
And here’s the output from running it:
Eshell V10.4.1 (abort with ^G)
1> c(hello).
{ok,hello}
2> hello:start(5).
X is 5!
ok
Now let’s try something more functional - instead of printing the value we will print its absolute value, so negative numbers lose their sign. Mathematically, abs(X) is defined as X if X >= 0, and -X if X < 0. Let’s implement this in Erlang, with a few intentional bugs along the way and using various styles.
Erlang has no return statement, the return value of a function is the value of its last statement. So we can implement an absolute value function that works for positive numbers (and zero) only as such:
abs_value(X) -> X.
To bring it closer to the mathematical definition, let’s define multiple versions - and for that we’ll need two things: the semicolon operator, and the when
keyword. Here’s what the code will look like:
abs_value(X) when X < 0 -> -X;
abs_value(X) when X > 0 -> X.
Note how the first definition ends with a semicolon, indicating that there’s another definition yet to come, while the second definition ends with a period. Function calls in Erlang use pattern matching, and the when
keyword offers some control over when to match or not. Let’s update our program:
-module(hello).
-export([start/1]).
abs_value(X) when X < 0 -> -X;
abs_value(X) when X > 0 -> X.
start(X) -> io:format("abs(X) is ~w!~n", [abs_value(X)]).
And run that through the Erlang shell:
Eshell V10.4.1 (abort with ^G)
1> c(hello).
{ok,hello}
2> hello:start(4).
abs(X) is 4!
ok
3> hello:start(-5).
abs(X) is 5!
ok
4> hello:start(0).
** exception error: no function clause matching hello:abs_value(0) (hello.erl, line 4)
in function hello:start/1 (hello.erl, line 7)
Note the failure here - we never specified what abs_value
should do when given exactly zero, and Erlang’s approach to error handling is to crash as soon as something unexpected has been encountered. This might sound counter-intuitive, but as much as I hate saying this it will make more sense as our code becomes more Erlang-like.
Right now, if you want to call the abs_value
function directly you’ll get an error:
5> hello:abs_value(5).
** exception error: undefined function hello:abs_value/1
This is because abs_value
isn’t exported, so let’s add it to the exports list to make our testing easier.
-export([start/1, abs_value/1]).
Fixing the issue with zero in abs_value
should be trivial:
abs_value(X) when X < 0 -> -X;
abs_value(X) when X >= 0 -> X.
And now let’s re-compile and test abs_value
directly:
Eshell V10.4.1 (abort with ^G)
1> c(hello).
{ok,hello}
2> hello:abs_value(3).
3
3> hello:abs_value(-4).
4
4> hello:abs_value(0).
0
Beyond returning a value, functions need a way to return state indicators like success, failure, etc. For this (and much more), Erlang has the concept of atoms - named objects that only assign to and equate with themselves and almost always start with a lowercase letter. Let’t play with them in the shell for a bit:
1> foo.
foo
2> bar.
bar
3> foo = foo.
foo
4> bar = bar.
bar
5> foo = bar.
** exception error: no match of right hand side value bar
Let’s try to use them in practice for pattern matching. Since we can do absurd things, let’s try to make the abs_value
function require that the caller specify whether the number provided is positive or negative, and match on that using atoms instead of when
. Here’s what the new code will look like:
-module(hello).
-export([abs_value/2]).
abs_value(negative, X) -> -X;
abs_value(positive, X) -> X.
And let’s look at the results of calling this new function:
2> hello:abs_value(negative, -4).
4
3> hello:abs_value(positive, 4).
4
4> hello:abs_value(negative, 6).
-6
5> hello:abs_value(foo, 1234).
** exception error: no function clause matching hello:abs_value(foo,1234) (hello.erl, line 4)
The matching based on atoms enabled an effect similar to overloading based on types in C++, and can be used to establish things like unit systems where a convert_to_celsius
function can require the input scale (celsius, fahrenheit, kelvin) to be provided as an atom argument and match to different implementations based on it.
Atoms also play a role in error handling (where by handling I mean the Erlang idea of crash-on-failure). Let’s define and export two new functions:
success() -> ok.
failure() -> error.
They work exactly as you’d expect, returning an atom and not much else:
2> hello:success().
ok
3> hello:failure().
error
4> ok = hello:success().
ok
Assigning an atom you expect to a call that returns it is how errors are handled. Let’s set ourselves up for failure and see what happens:
5> ok = hello:failure().
** exception error: no match of right hand side value error
This is the same as writing ok = error.
or foo = bar.
above - it doesn’t match and therefore the process crashes. It’s akin to a C codebase filled with assertion where the first unexpected input or failure will crash the application. In C, a crash is fatal but in Erlang a crash is usually an indicator for “let’s restart and try this again”. If you think this doesn’t sound right, I was in the same boat and I recommend reading Jim Gray’s Why Do Computers Stop and What Can Be Done About It?.
And that’s all for Part I. In part II we’ll explore more Erlang-like code and some basic concurrent algorithms in Erlang.
The code for this post can be found on GitHub.