Messaging in Erlang

|

Erlang is a language that has fascinated me for lo, these last few weeks. For the unfamiliar, it was developed to implement high-availability, distributed, fault-tolerant systems for the telecom world decades ago and in recent years has witnessed a resurgence as multi-core processors become the norm, rather than the exception.

At first, Erlang is bit daunting. Variables aren't, line endings vary by location, and recursion is common-place, among others. After a few hours of playing with the language however, these 'problems' turn into 'features' and you begin to see the benefits of the language.

A key component of Erlang are processes. Processes, unlike threads in other languages, have no shared environment or data. The only manner of communicating things between processes is through messaging. Processes listen for messages addressed to them and take appropriate action. I've been playing with inter-processes messaging and wanted to share a simple example. First, I'll list the code and then walk you through it. The code is based on the 'Getting Started' tutorial at http://www.erlang.org

The Code:

-module(messagetest).
-export([start/0, first/2, second/0]).

first(N, SecondPID) ->
  SecondPID ! {N + 1, self()},
  receive
    continue ->
      io:format("First Continuing... ~n", []),
      first(N + 1, SecondPID);
    stop ->
      io:format("First Received 'Stop' command.  Stopping.~n", []),
      SecondPID ! done
  end.

second() ->
  receive
    done ->
      io:format("Second Received 'done'~n", []);
    {N, FirstPID} ->
      case (N < 10) of
        false -> FirstPID ! stop;
        true -> FirstPID ! continue
      end,
      io:format("Second got ~p ~n", [N]),
      second()
  end.

start() ->
  SecondPID = spawn(messagetest, second, []),
  spawn(messagetest, first, [0, SecondPID]).

The Explanation:

The first two lines define the module name and give a directory of the functions that are externally callable along with their argument counts. The last three lines are where the fun begins. Save the code in a file named 'messagetest.erl' and from the Erlang interpreter, compile it by typing:

c(messagetest). 

Next, run the 'start' function by typing:

messagetest:start().

You'll see output similar to:

<0.71.0>Second got 1 

First Continuing... 
Second got 2 
First Continuing... 
Second got 3 
First Continuing... 
Second got 4 
First Continuing... 
Second got 5 
First Continuing... 
Second got 6 
First Continuing... 
Second got 7 
First Continuing... 
Second got 8 
First Continuing... 
Second got 9 
First Continuing... 
Second got 10
First Received 'Stop' command.  Stopping. 
Second Received 'done'

The <0.71.0> is the process id (not to be confused with a *nix PID) and will very likely be different on your machine. Let's look at the 'start' function.

start() ->
  SecondPID = spawn(messagetest, second, []),
  spawn(messagetest, first, [0, SecondPID])

The first line is the function definition. Simple enough. The second line executes the spawn command which kicks off an Erlang process that executes the function called 'second' in the 'messagetest' module. No parameters are passed ('[]'). The return value of spawn is the process ID of the started process.

The second line also spawns another Erlang process, passing it a value of 0 as well as the process ID of the process that was just spawned.

Ironically enough, we'll examine the function called 'second' first as it's a bit easier to understand, I think. 'second' consists entirely of a receive/end block who's job is to receive messages passed to the function. Inter-process messages are sent via:

process_id_of_recipient ! message

When a process receives a message, it is placed in a FIFO queue and the next message is examined when a receive/end is encountered. Message processing is very similar to a case/switch in other languages in that the message is compared against a list of expected values and when a match is found the corresponding code is evaluated. If no match is found, the next message is examined while keeping the first message in the queue. If the second doesn't match, the third is examined and so on until either a message matches at which point it is removed from the queue, or there are no more messages. In the case of the latter, the process blocks and waits for a message it knows how to process.

second() ->
  receive
    done ->
      io:format("Second Received 'done'~n", []);
    {N, FirstPID} ->
      case (N < 10) of
        false -> FirstPID ! stop;
        true -> FirstPID ! continue
      end,
      io:format("Second got ~p ~n", [N]),
      second()
  end.

Here we can see that the function is written to respond to either the word 'done' or a tuple matching a {x,y} pattern. In the case of the word 'done' for example, a notice is printed and the function ends.

In the event the second pattern matches the message, the value of 'N' is evaluated to see if it's less than 10. If it isn't, a 'stop' message is sent to whatever process has a PID of 'FirstPID' - I'll show where this comes from in a moment. If it is, the message 'continue' is sent instead.

In either case, the number it received in the variable 'N' is printed and the function calls itself to begin listening again. Now over to the 'first' function.

first(N, SecondPID) ->
  SecondPID ! {N + 1, self()},
  receive
    continue ->
      io:format("First Continuing... ~n", []),
      first(N + 1, SecondPID);
    stop ->
      io:format("First Received 'Stop' command.  Stopping.~n", []),
      SecondPID ! done
  end.

'first' expects two parameters, a number and a PID. As you recall from the 'start' function, this information was supplied by calling:

spawn(messagetest, first, [0, SecondPID]).

'first' uses:

SecondPID ! {N + 1, self()},
to send a message to the process with a PID of 'SecondPID' consisting of the number it received plus one and it's own PID. It then enters a receive/end loop to wait for a response from the 'second' function. As you remember, 'second' checks to see if this number is greater than 10 and sends either 'stop' or 'continue' accordingly. In this case, the number is 1 so 'second' tells 'first' to continue.

This matches the

continue ->
  io:format("First Continuing... ~n", []),
  first(N + 1, SecondPID);

block, so a message is printed and 'first' calls itself passing N + 1 and the PID of the 'second' function's process. This cycle repeats until 'first' tells 'second' it's reached the number 10.

At this point, 'second' send a message consisting of 'stop' back to 'first'.

stop ->
  io:format("First Received 'Stop' command.  Stopping.~n", []),
  SecondPID ! done

'first' processes the message and prints that it is shutting down. Finally, the message 'done' is sent back to 'second'. 'first' shuts itself down as it has nothing left to do.

done ->
  io:format("Second Received 'done'~n", []);

'second' receives the 'done' message from 'first', prints an acknowledgment, and decides it's going out for a beer or something so it shuts down. The program terminates.

There's a lot going on and it can take some time to wrap your head around it, but probably significantly less time than you might think. I find Erlang to be really enjoyable to write, although I think the error messages could use some work. The people who wrote Erlang apparently founded the 'Obscure Error Message College' (note: in the 1990s, it was renamed to 'The Javascript "Has No Properties" Academy').

I hope this was useful to someone and that you enjoy working in Erlang.

About this Entry

This page contains a single entry by Philip Ratzsch published on April 19, 2008 10:11 AM.

Ruby Vector extensions, DNS was the previous entry in this blog.

man page humor is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.