Tutorial: Lightweight Processes

Due to its Erlang foundation, an LFE program is composed of anywhere from 1 to hundreds of thousands of lightweight processes. In fact, a 2005 message to comp.lang.functional reported spawning 20 million messages in a ring benchmark (on a 1.5 GHz SPARC with 16 GB RAM).

This is possible in part because each process operates independently with its own private memory, communicating via message passing, and the overhead for an Erlang process is pretty low (~1.5k for 32-bit and ~2.7k for 64-bit; see the Efficiency Guide's section on processes for more info).

This tutorial aims to decrease the mystery around Erlang processes and how to use them in LFE programs.

1 Interacting with Processes

1.1 Dump and flush

Processes in LFE are built from functions. These running functions communicate with other running functions via messages. When you start up the LFE REPL, you're using an Erlang process, and you can communicate with it just like any other process.

Let's get the REPL's process id and then send messages to the REPL using the PID:

> (set pid (self))
<0.30.0>
> (! pid '"Testing: 1, 2, 3!")
"Testing: 1, 2, 3!"
> (! pid '"This is another test message...")
"This is another test message..."
> (! pid (list 1 2 3))
(1 2 3)
>

The messages are sitting in the inbox of the process they were sent to, the REPL. If we flush the REPL's inbox, we can see them:

> (: c flush)
Shell got "Test1"
Shell got "Testing: 1, 2, 3!"
Shell got "This is another test message..."
Shell got [1,2,3]
ok
>

1.2 Getting Classy with receive

As you might imagine, there's a better way to do this. Let's send another message to the REPL's message queue (inbox):

> (! pid (list 1 2 3))
(1 2 3)
>

Now let's take a look at that message without flushing the queue:

> (receive
    ((list a b c)
     (: io format '"results: ~p ~p ~p~n" (list a b c))))
results: 1 2 3
ok
>

If there is a message in the inbox matching the pattern we have defined (in this case, a list of length 3), then we will have access to the data that is matched and bound to the variables. For more information on pattern matching, see the tutorial.

If there are a bunch of messages in the inbox, they will all be iterated over until a match is found:

> (set pid (self))
<0.30.0>
> (! pid (tuple 1 2 3))
#(1 2 3)
> (! pid (tuple 2 3))
#(2 3)
> (! pid (tuple 3))
#(3)
> (receive
    ((tuple a)
     (: io format '"results: ~p ~n" (list a))))
results: 3
ok

Let's confirm that only the last message we entered was matched:

> (: c flush)
Shell got {1,2,3}
Shell got {2,3}
ok
>

1.3 Shell spawn

So far, we've only look at the process for the REPL itself. We'd like to expand our horizons and look at creating a process in the REPL, writing to it instead of our shell.

However, we are faced with some difficulties: * LFE doesn't let us define functions (or macros) in the REPL, and * Erlang's spawn function takes a module and function as a parameter.

We can sort of get around that first point using lambda:

> (set print-result
    (lambda (msg)
      (: io format '"Received message: '~s'~n" (list msg))))
#Fun<lfe_eval.10.53503600>
> (funcall print-result '"Zaphod was here.")
Received message: 'Zaphod was here.'
ok
>

Let's update this function so that it can respond to messages when it's running as an Erlang process using the call to receive:

> (set print-result
    (lambda ()
      (receive
        (msg
          (: io format '"Received message: '~s'~n" (list msg))))))
#Fun<lfe_eval.21.53503600>
>

Now that we've got our message-capable function, let's spawn it and capture the process id so that we can write to it:

> (set pid (spawn (lambda () (funcall print-result))))
<0.66.0>
> (! pid '"Time is an illusion. Lunchtime doubly so.")
"Time is an illusion. Lunchtime doubly so."
Received message: 'Time is an illusion. Lunchtime doubly so.'
>

As you can see, when we send our message to the process we started with spawn, the process' function prints out the message that it received from us.

We had to go through some gymnastics here, due to the limitations of the shell and using funcall in a spawn call.

Up next: in an anti-intuitive twist, you'll see that doing the same thing from a module is more clear that doing it in the shell ;-)