Sherief, FYI

Erlang for Procedural Programmers, Part I

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.