Continuing on from yesterday with the Erlanging… Chapter 2 of the Erlang getting started documentation – Concurrent Programming.

(I realize that this is technically Chapter 3, but I’m not counting the introduction)

The chapter starts with little bit of background on what processes are and why they are useful. To paraphrase: processes are threads of execution that don’t share data/memory. Erlang processes pass messages between each other to allow for concurrency.

Creating a new process in Erlang is made super easy by using the built-in function spawn(Module, Exported_Function, List of Arguments). For example, if we wanted to spawn a process to run the function double/1 from the module tut from the last post, we would write:

spawn(tut, double, [2])

Assuming tut.beam (the module, as compiled by c(tut)) is available in the directory that we run this from, the process will execute double and get the return value 4.

Unfortunately, we have no way of accessing this return value from the parent process, as processes are memory independent! This is where sending messages and the receive construct come in.

Sending a Message

To send a message to a process, Erlang uses this pattern:

<PID> ! <message>.

Where <PID> is the Process ID or Registered Name of the receiving process and <message> is any valid Erlang data type.

The return value of spawn is the Process ID of the spawned process, and can be used to send that process messages. A process can retrieve its own Process ID by calling self().

The receiving process uses the receive keyword to act accordingly on the message it receives. This construct acts a bit like a switch statement, executing the path that the message’s structure matches:

receive
    <possible message pattern> ->
        <executed code>;
    <another pattern> ->
        <executed code>
end.

Here’s a (really) brief and simple example of this in action. Pretend the module’s name is “helloer”:

receiver() ->
    receive
        say_hello ->
            io:format("hello~n", []);
        say_goodbye ->
            io:format("goodbye~n", [])
    end.

main() ->
    Receiver_PID = spawn(helloer, receiver, []),
    Receiver_PID ! say_hello,
    Receiver_PID ! say_goodbye.

The console would look like this (starting with the call to main):

1> helloer:main().
hello
say_goodbye
2>

This is a little weird. First, processes know where the “most intelligent” place for output is, so even though the io:format calls are in the spawned process, they output to the current console.

The first line hello comes from the spawned process running receiver. The second line, say_goodbye, is the return value of main (functions return whatever their last value is). So where the hell is goodbye?

The thing is, processes pause when they hit receive until they receive a message that matches one of the patterns. However, once they do receive a matching message, they execute the corresponding actions, then return, and the process ends. This means, that if we want receiver to continue to await messages, it needs to be recursive:

receiver() ->
    receive
        say_hello ->
            io:format("hello~n", []);
        say_goodbye ->
            io:format("goodbye~n", [])
    end,
    receiver().

Because Erlang is tail-call optimized, this infinite recursion is essentially a while loop.

Messages can be any Erlang data structure, so if a message sender wants to include a “return stamp” for the receiver to send finished work back to, they can just include their own Process ID in the message:

PID ! {<data>, self()}.

Rather than tediously using a Process ID to refer to a process, a spawned process can be registered under an atom, and can be referred to by that atom module-wide:

register(name, spawn(module, function, [args])).

Communication Across Networks

Erlang has built-in distributed processing using a magic cookie. The easiest way to accomplish this is by having a file named .erlang.cookie with permissions 400 (read permission only, only for owner) in the executing user’s home directory on each computer. The file can contain anything as long as it is identical in every location.

Using this method, I was able to easily send a message from my laptop to my Android phone on the same WiFi network by giving each Erlang node a “name”.

On laptop:

$ erl -name [email protected]
1> c(helloer).
{ok,helloer}
2> register(receiver, spawn(helloer, receiver, [])).
true
3>

On phone:

$ erl -name [email protected]
1> net_kernel:connect_node('[email protected]').
2> {receiver, '[email protected]'} ! say_hello.

Back on laptop:

hello
3>

Pretty neat!

The chapter ends with an extended example of using the above techniques to implement a simple Eshell messenger “app”. I highly recommend going through it yourself.

Next time, I’ll be going through the (somewhat shorter) chapter on Robustness. Later!

— M