Chapter 8. Playing with Processes

While Erlang is a functional language, Erlang programs are rarely structured around simple functions. Instead, Erlang’s key organizational concept is the process, an independent component (built from functions) that sends and receives messages. Programs are deployed as sets of processes that communicate with each other. This approach makes it much easier to distribute work across multiple processors or computers, and also makes it possible to do things like upgrade programs in place without shutting down the whole system.

But taking advantage of those features means learning how to create (and end) processes, how to send messages among them, and how to apply the power of pattern matching to incoming messages.

The Shell Is a Process

You’ve been working within a single process throughout this book so far, the Erlang shell. None of the previous examples sent or received messages, of course, but the shell is an easy place to send and (for test purposes, at least) receive messages.

The first thing to explore is the process identifier, often called a pid. The easiest pid to get is your own. In the shell you can just run the self() function:

1> self().
<0.36.0>

<0.36.0> is the shell’s representation of a triple, a set of three integers that provide the unique identifier for this process. (You will probably get a different set of numbers when you try it, though.) This group of numbers is guaranteed to be unique within this run of Erlang, but are not guaranteed to be the same in the future. Erlang uses pids internally, but while you can read them in the shell, you can’t type pids directly into the shell or into functions. Erlang much prefers that you treat pids as abstractions, though if you really want to address a process by its pid number, you can use the pid/3 shell function to do so.

Every process gets its own pid, and those pids function like addresses for mailboxes. Your programs will send messages from one process to another by sending them to a pid. When that process has time to check its mailbox, it will be able to retrieve and process the messages there.

Erlang, however, will never report that a message send failed, even if the pid doesn’t point to a real process. It also won’t report that a message was ignored by a process. It’s up to you to make sure your processes are assembled correctly.

Note

Pids can even identify processes running on multiple computers within a cluster. You’ll need to do more work to set up a cluster, but when you get there you won’t have to throw away code you wrote with pids and processes built on them.

The syntax for sending a message is pretty simple: a function or variable containing the pid, plus the send operator (!) and the message.

2> self() ! test1.
test1
3> Pid=self().
<0.36.0>
4> Pid ! test2.
test2

Line 2 sent a message to the shell containing the atom test1. Line 3 assigned the pid for the shell, retrieved with the self() function, to a variable named Pid, and then line 4 used that Pid variable to send a message containing the atom test2. (The ! always returns the message, which is why it appears right after the sends in lines 2 and 4.)

Where did those messages go? What happened to them? Right now, they’re just waiting in the shell’s mailbox, doing nothing.

There’s a shell function—flush()—that you can use to see what’s in the mailbox, though it also removes those messages from the mailbox. The first time you use it, you’ll get a report of what’s in the mailbox, but the second time, the messages are gone, already read:

5> flush().
Shell got test1
Shell got test2
ok
6> flush().
ok

The proper way to read the mailbox, which gives you a chance to do something with the messages, is the receive…end construct, which puts the message content into a variable and lets you process it. You can test this out in the shell. The first of the following tests just reports what the message was, whereas the second expects a number and doubles it:

7> self() ! test1.
test1
8> receive X -> X end.
test1
9> self() ! 23.
23
10> receive Y->2*Y end.
46

So far, so good. However, if you make a mistake—if there isn’t a message waiting, or if you provide a pattern match that doesn’t work—the shell will just sit there, hung. Actually, it’s waiting for something to arrive in the mailbox, but you’ll be stuck. The easiest way out of that is to hit Ctrl-G, and then type q. You’ll have to restart Erlang.

Another way to hang the system, one that can become mysterious in more complicated contexts, happens if you try to reuse X and Y after they become bound variables. For example, you can do this without a problem:

1> self() ! test1.
test1
2> receive X -> X end.
test1
3> self() ! test1.
test1
4> receive X -> X end.
test1

But if you change the value that goes to X the next time, Erlang isn’t happy and will just hang on you:

5> self() ! test2.
test2
6> receive X -> X end.

The first time around, X was unbound and able to accept any value. The second time, X was bound, but could still accept the same value, in this case test1. The last time, X was bound, and it couldn’t accept a new value, test2, so it didn’t, and the console got stuck.

Spawning Processes from Modules

While sending messages to the shell is an easy way to see what’s happening, it’s not especially useful. Processes at their heart are just functions, and you know how to build functions in modules. The receive…end statement is structured like a case…end statement, so it’s easy to get started.

Example 8-1, which is in ch08/ex1-simple, shows a simple—excessively simple—module containing a function that reports messages it receives.

Example 8-1. An overly simple process definition
-module(bounce).
-export([report/0]).

report() ->
  receive
     X -> io:format("Received ~p~n",[X])
  end.

When the report/0 function receives a message, it will report that it received it. Setting this up as a process means compiling it and then using the spawn/3 function, which turns the function into a free-standing process. The arguments for spawn/3 are the module name, the function name, and a list of arguments for the function. Even if you don’t have any arguments, you need to include an empty list in square brackets, and a single argument should be a one-item list. The spawn/3 function will return the Pid, which you should capture in a variable, here Pid:

1> c(bounce).
{ok,bounce}
2> Pid=spawn(bounce,report,[]).
<0.38.0>
3> is_process_alive(Pid).
true

You can test to see if your process is alive with the erlang:is_process_alive/1 function:

3> is_process_alive(Pid).
true

Once you have the process spawned, you can send a message to that pid, and it will report that it received it:

4> Pid ! 23.
Received 23
23
ok

However, there’s one small problem. The report process exited—it went through the receive clause only once, and when it was done, was done. If you try to send it a message, the message will be returned, and nothing will report an error, but you also won’t get any notification that the message was received because nothing is listening any longer:

5> Pid ! 23.
23
6> is_process_alive(Pid).
false

To create a process that keeps processing messages, you need to add a recursive call, as shown in the receive statement in Example 8-2, in ch08/ex2-recursion.

Example 8-2. A function that creates a stable process
-module(bounce).
-export([report/0]).

report() ->
  receive
     X -> io:format("Received ~p~n",[X]),
          report()
  end.

That extra call to report() means that after the function shows the message that arrived, it will run again, ready for the next message. If you recompile the bounce module and spawn it to a new Pid2 variable, you can send it multiple messages, as shown here:

5> c(bounce).
{ok,bounce}
6> Pid2=spawn(bounce,report,[]).
<0.47.0>
7> Pid2 ! 23.
Received 23
23
ok
8> Pid2 ! message.
Received message
message

You can also pass an accumulator from call to call if you want, for a simple example, to keep track of how many messages have been received by this process. Example 8-3 shows the addition of an argument; in this case, just an integer that gets incremented with each call. You can find it in ch08/ex3-counter.

Example 8-3. A function that adds a counter to its message reporting
-module(bounce).
-export([report/1]).

report(Count) ->
  receive
     X -> io:format("Received #~p: ~p~n",[Count,X]),
          report(Count+1)
  end.

The results are pretty predictable, but remember that you need to include an initial value in the arguments list in the spawn/3 call:

1> c(bounce).
{ok,bounce}
2> Pid2=spawn(bounce,report,[1]).
<0.38.0>
3> Pid2 ! test.
Received #1: test
test
ok
4> Pid2 ! test2.
Received #2: test2
test2
ok
5> Pid2 ! another.
Received #3: another

Whatever you do in your recursive call, keeping it simple (and preferably tail-recursive) is best, as these can get called many, many times in the life of a process.

Note

If you want to create impatient processes that stop after waiting a given amount of time for a message, you should investigate the after construct of the receive clause.

You can write this function in a slightly different way that may make what’s happening clearer and easier to generalize. Example 8-4, in ch08/ex4-state, shows how to use the return value of the receive clause, here the Count plus one, to pass state from one iteration to the next.

Example 8-4. Using the return value of the receive clause as state for the next iteration
-module(bounce).
-export([report/1]).

report(Count) ->
  NewCount = receive
     X -> io:format("Received #~p: ~p~n",[Count,X]),
          Count + 1
  end,
  report(NewCount).

In this model, all (though just one here) of the receive clauses return a value that gets passed to the next iteration of the function. If you use this approach, you can think of the return value of the receive clause as the state to be preserved between function calls. That state can be much more intricate than a counter—it might be a tuple, for instance, that includes references to important resources or work in progress.

Lightweight Processes

If you’ve worked in other programming languages, you may be getting worried. Threads and process spawning are notoriously complex and often slow in other contexts, but Erlang expects applications to be a group of easily spawned processes? That run recursively?

Yes, absolutely. Erlang was written specifically to support that model, and its processes weigh less than its competitors. Erlang processes are designed to impose absolutely minimal overhead cost. The Erlang scheduler gets processes started and distributes processing time among them, and also splits them out across multiple processors.

It is certainly possible to write processes that perform badly and to structure applications so that they wait a long time before doing anything. But you don’t have to worry about those problems happening just because you’re using multiple processes.

Registering a Process

Much of the time, pids are all you need to find and contact a process. However, you will likely create some processes that need to be more findable. Erlang provides a process registration system that is extremely simple: you specify an atom and a pid, and then any process that wants to reach that registered process can just use the atom to find it. This makes it easier, for example, to add a new process to a system and have it connect with previously existing processes.

To register a process, use the register/2 built-in function. The first argument is an atom, effectively the name you’re assigning the process, and the second argument is the pid of the process. Once you have it registered, you can send it messages, using the atom instead of a pid:

1> Pid1=spawn(bounce,report,[1]).
<0.33.0>
2> register(bounce,Pid1).
true
3> bounce ! hello.
Received #1: hello
hello
ok
4> bounce ! "Really?".
Received #2: "Really?"
"Really?"
ok

If you attempt to call a process that doesn’t exist (or one that has crashed), you’ll get a bad argument error:

6> zingo ! test.
** exception error: bad argument
     in operator  !/2
        called as zingo ! test

If you attempt to register a process to a name that is already in use, you’ll also get an error, but if a process has exited (or crashed), the name is effectively no longer in use and you can re-register it.

You can also use whereis/1 to retrieve the pid for a registered process (or undefined, if there is no process registered with that atom), and unregister/1 to take a process out of the registration list without killing it:

5> GetBounce = whereis(bounce).
<0.33.0>
6> unregister(bounce).
true
7> TestBounce = whereis(bounce).
undefined
8> GetBounce ! "Still there?".
Received #3: "Still there?"
"Still there?"
ok
Note

If you want to see which processes are registered, you can use the regs() shell command.

If you’ve worked in other programming languages and learned the gospel of “no global variables,” you may be wondering why Erlang permits a systemwide list of processes like this. Most of the first half of this book, after all, has been about isolating change and minimizing shared context.

If you think of registered processes as more like services than functions, however, it may make more sense. A registered process is effectively a service published to the entire system, something usable from multiple contexts. Used sparingly, registered processes create reliable entry points for your programs, something that can be very valuable as your code grows in size and complexity.

When Processes Break

Processes are fragile. If there’s an error, the function stops and the process goes away. Example 8-5, in ch08/ex5-division, shows a report/0 function that can break if it gets input that isn’t a number.

Example 8-5. A fragile function
-module(bounce).
-export([report/0]).

report() ->
  receive
     X -> io:format("Divided to ~p~n",[X/2]),
          report()
  end.

If you compile and run this (deliberately) error-inviting code, you’ll find that it works well so long as you only send it numbers. Send anything else, and you’ll see an ERROR REPORT in the shell, and no more responses from that pid. It died:

1> c(bounce).
{ok,bounce}
2>  Pid3=spawn(bounce,report,[]).
<0.38.0>
3> Pid3 ! 38.
Divided to 19.0
38
ok
4> Pid3 ! 27.56.
Divided to 13.78
27.56
ok
5> Pid3 ! seven.

=ERROR REPORT==== 24-Aug-2016::20:59:43 ===
Error in process <0.38.0> with exit value: {badarith,[{bounce,report,0,[{file,
"bounce.erl"},{line,6}]}]}

seven
6> Pid3 ! 14.
14

As you get deeper into Erlang’s process model, you’ll find that “let it crash” is not an unusual design decision in Erlang, though being able to tolerate such errors and continue requires some extra work. Chapter 9 will show you how to find and deal with errors of various kinds.

Processes Talking Amongst Themselves

Sending messages to Erlang processes is easy, but it’s hard for them to report back responses if you don’t leave information about where they can find you again. Sending a message without including the sender’s pid is kind of like leaving a phone message without including your own number: it might trigger action, but the recipient might not get back to you.

To establish process-to-process communications without registering lots of processes, you need to include pids in the messages. Passing the pid requires adding an argument to the message. It’s easy to get started with a test that calls back the shell. Example 8-6, in ch08/ex6-talking, builds on the drop module from Example 3-2, adding a drop/0 function that receives messages and removing the fall_velocity/2 function from the export.

Example 8-6. A process that sends a message back to the process that called it
-module(drop).
-export([drop/0]).

drop() ->
 receive
   {From, Planemo, Distance} ->
     From ! {Planemo, Distance, fall_velocity(Planemo, Distance)},
     drop()
 end.

fall_velocity(earth, Distance) when Distance >= 0  -> math:sqrt(2 * 9.8 * Distance);
fall_velocity(moon, Distance) when Distance >= 0 -> math:sqrt(2 * 1.6 * Distance);
fall_velocity(mars, Distance) when Distance >= 0 -> math:sqrt(2 * 3.71 * Distance).

To get started, it’s easy to test this from the shell:

1> c(drop).
{ok,drop}
2> Pid1=spawn(drop,drop,[]).
<0.38.0>
3> Pid1 ! {self(), moon, 20}.
{<0.31.0>,moon,20}
4> flush().
Shell got {moon,20,8.0}
ok

Example 8-7, which you’ll find in ch08/ex7-talkingProcs, shows a process that calls that process to demonstrate that this can work with more than just the shell.

Example 8-7. Calling a process from a process, and reporting the results
-module(mph_drop).
-export([mph_drop/0]).

mph_drop() ->
  Drop=spawn(drop,drop,[]),
  convert(Drop).

convert(Drop) ->
 receive
   {Planemo, Distance} ->
     Drop ! {self(), Planemo, Distance},
     convert(Drop);
   {Planemo, Distance, Velocity} ->
     MphVelocity = 2.23693629 * Velocity,
     io:format("On ~p, a fall of ~p meters yields a velocity of ~p mph.~n",
     [Planemo, Distance, MphVelocity]),
     convert(Drop)
 end.

The mph_drop/1 function spawns a drop:drop/0 process when it is first set up, using the same module you saw in Example 8-6, and stores the pid in Drop. Then it calls convert/1, which will also listen for messages recursively.

Warning

If you don’t separate the initialization from the recursive listener, your code will work, but will spawn new drop:drop/0 processes every time it processes a message instead of using the same one repeatedly.

The receive clause relies on the call from the shell (or another process) including only two arguments, while the drop:drop/0 process sends back a result with three. When the receive clause gets a message with two arguments, it sends a message to Drop, identifying itself as the sender and passing on the arguments. When the Drop returns a message with the result, the receive clause reports on the result, converting the velocity to miles per hour. (Yes, it leaves the distance metric, but makes the velocity more intelligible to Americans.)

Note

As your code grows more complex, you will likely want to use more explicit flags about the kind of information contained in a message, like atoms.

Running Example 8-7 from the shell looks like the following:

1> c(drop).
{ok,drop}
2> c(mph_drop).
{ok,mph_drop}
3> Pid1=spawn(mph_drop,mph_drop,[]).
<0.59.0>
4> Pid1 ! {earth,20}.
On earth, a fall of 20 meters yields a velocity of 44.289078952755766 mph.
{earth,20}
5> Pid1 ! {mars,20}.
On mars, a fall of 20 meters yields a velocity of 27.250254686571544 mph.
{mars,20}

This simple example might look like it behaves as a more complex version of a function call, but there is a critical difference. In the shell, with nothing else running, the result will come back quickly—so quickly that it reports before the shell puts up the message—but this was a series of asynchronous calls. Nothing held and waited specifically for a returned message.

The shell sent a message to Pid1, the process identifier for mph_drop:convert/1. That process sent a message to Drop, the process identifier for drop:drop/0, which mph_drop:mph_drop:0 set up when it was spawned. That process returned another message to mph_drop:convert/1, which reported to standard output, in this case the shell. Those messages passed and were processed rapidly. However, in a system with thousands or millions of messages in motion, those passages might have been separated by many messages, and come in later.

Watching Your Processes

Erlang provides a simple but powerful tool for keeping track of your processes and seeing what’s happening. It’s called Observer, and it offers a minimal GUI that lets you look into the current state of your processes and see what’s happening. Depending on how you installed Erlang, you may be able to start it from a toolbar, but you can always start it from the shell:

6> observer:start().
ok

You’ll see something like Figure 8-1 appear, presenting an overview of your Erlang system. To get to your processes, click the Processes tab and then the “Name or Initial function” label to sort the list by process name. You may need to scroll down a little to find drop:drop/0, but you’ll see something similar to Figure 8-2.

ier2 0801
Figure 8-1. Observer at startup
ier2 0802
Figure 8-2. Observer’s process window, sorted by process name

The list of processes is useful, but Observer also lets you look inside of process activity. If you double-click on a process, say mph_drop:mph_drop/0, you’ll get some basic information about the process, as shown in Figure 8-3.

Figuring out what your process is doing, however, requires enabling tracing. First, find the MphDrop:mph_drop/0 process in the list of processes, right-click it, choose “Trace selected processes by name (all nodes),” and select the options shown in Figure 8-4. Then click OK. You’ll be back to the main window, where you should click the Trace Overview tab. Then click Start Trace. You will get a warning message, but you can ignore it. The Trace Log window will open, probably saying something like “Dropped Messages.”

ier2 0803
Figure 8-3. A closer look at mph_drop
ier2 0804
Figure 8-4. Basic trace options

Now you can see how messages flow.

7> Pid1 ! {earth,20}.
On earth, a fall of 20 meters yields a velocity of 44.289078952755766 mph.
{earth,20}
8> Pid1 ! {mars,20}.
On mars, a fall of 20 meters yields a velocity of 27.250254686571544 mph.
{mars,20}

The Observer Trace Log window for that process will update to show messages and calls, as shown in Figure 8-5. Just as in the normal Erlang syntax, ! means a message is sent. << means a message is received.

ier2 0805
Figure 8-5. Tracing calls when you send mph_drop a message

The chain of messages starts with the call from the shell to <0.59.0>, with a tuple containing earth and 20. <0.59.0> sends that same tuple to <0.60.0>, which sends back the metric version of the velocity calculation. Because the trace is only following <0.59.0>, this gets reported with a << (a receive) with a tuple containing three values. Three values mean it’s time to report. io:format turns out to send its own messages. A complex tuple starting with io_request brings the result back to the shell, and then the whole process repeats for the call about Mars.

Observer is generally the easiest place to turn when you’re having difficulty figuring out what is happening among your processes.

Breaking Things and Linking Processes

When you send a message, you’ll always get back the message as the return value. This doesn’t mean that everything went well and the message was received and processed correctly, however. If you send a message that doesn’t match a pattern at the receiving process, nothing will happen (for now at least); the message will land in the mailbox but not trigger activity. Sending a message that gets through the pattern matching but creates an error will halt the process where the error occurred, possibly even a few messages and processes down the line.

Note

Messages that don’t match a pattern in the receive clause don’t vanish; they just linger in the mailbox without being processed. It is possible to update a process with a new version of the code that retrieves those messages.

Because processes are fragile, you often want your code to know when another process has failed. In this case, if bad inputs halt the drop:drop/0, it doesn’t make much sense to leave the mph_drop:convert/1 process hanging around. You can see how this works through the shell and Observer. First, start up Observer and spawn mph_drop:mph_drop/0.

1> observer:start().
ok
2> Pid1=spawn(mph_drop,mph_drop,[]).
<0.83.0>

If you switch to the Processes tab and click on the Pid header to sort them, you’ll see something like Figure 8-6 in Observer. Then, feed your process some bad data, an atom (zoids) instead of a number for the Distance, and Observer will look more like Figure 8-7:

3> Pid1 ! {moon,zoids}.
{moon,zoids}
4>

=ERROR REPORT==== 19-Dec-2016::21:03:36 ===
Error in process <0.85.0> with exit value:
{badarith,[{drop,fall_velocity,2,[{file,"drop.erl"},{line,12}]},
           {drop,drop,0,[{file,"drop.erl"},{line,7}]}]}

Since the remaining mph_drop:convert/1 process is now useless, it would be better for it to halt when drop:drop/0 fails. Erlang lets you specify that dependency with a link. The easy way to do that while avoiding making your code race with itself is to use spawn_link/3 instead of just spawn/3. This is shown in Example 8-8, which you can find in ch08/ex8-linking.

Example 8-8. Calling a linked process from a process so failures propagate
-module(mph_drop).
-export([mph_drop/0]).

mph_drop() ->
  Drop=spawn_link(drop,drop,[]),
  convert(Drop).

convert(Drop) ->
 receive
   {Planemo, Distance} ->
     Drop ! {self(), Planemo, Distance},
     convert(Drop);
   {Planemo, Distance, Velocity} ->
     MphVelocity= 2.23693629 * Velocity,
     io:format("On ~p, a fall of ~p meters yields a velocity of ~p mph.~n",
 [Planemo, Distance, MphVelocity]),
     convert(Drop)
 end.
ier2 0806
Figure 8-6. A healthy set of processes
ier2 0807
Figure 8-7. Only the drop:drop/0 process is gone

Now, if you recompile and test this out with Observer, you’ll see that both processes vanish when drop:drop/0 fails, as shown in Figure 8-8:

1> c(drop).
{ok,drop}
2> c(mph_drop).
{ok,mph_drop}
3> observer:start().
ok
4> Pid1=spawn(mph_drop,mph_drop,[]).
<0.1004.0>
5> Pid1 ! {moon,zoids}.
{moon,zoids}
6>
=ERROR REPORT==== 19-Dec-2016::21:09:01 ===
Error in process <0.1005.0> with exit value:
{badarith,[{drop,fall_velocity,2,[{file,"drop.erl"},{line,12}]},
           {drop,drop,0,[{file,"drop.erl"},{line,7}]}]}
ier2 0808
Figure 8-8. Both processes now depart when there is an error
Note

Links are bidirectional. If you kill the the mph_drop:mph_drop/0 process—with, for example, exit(Pid1,kill).—the drop:drop/1 process will also vanish. (kill is the harshest reason for an exit, and can’t be stopped because sometimes you really need to halt a process.)

That kind of failure may not be what you have in mind when you think of linking processes. It’s the default behavior for linked Erlang processes, and makes sense in many contexts, but you can also have a process trap exits. When an Erlang process fails, it sends an explanation to other processes that are linked to it in the form of a tuple. The tuple contains the atom EXIT, the Pid of the failed process, and the error as a complex tuple. If your process is set to trap exits, through a call to process_flag(trap_exit, true), these error reports arrive as messages, rather than just killing your process.

Example 8-9, in ch08/ex9-trapping, shows how the initial mph_drop/0 method changes to include this call to set the process flag, and adds another entry to the receive clause that will listen for exits and report them more neatly.

Example 8-9. Trapping a failure, reporting an error, and exiting
-module(mph_drop).
-export([mph_drop/0]).

mph_drop() ->
  process_flag(trap_exit, true),
  Drop=spawn_link(drop,drop,[]),
  convert(Drop).

convert(Drop) ->
 receive
   {Planemo, Distance} ->
     Drop ! {self(), Planemo, Distance},
     convert(Drop);
   {'EXIT', Pid, Reason} ->
     io:format("FAILURE: ~p died because of ~p.~n",[Pid, Reason]);
   {Planemo, Distance, Velocity} ->
     MphVelocity= 2.23693629 * Velocity,
     io:format("On ~p, a fall of ~p meters yields a velocity of ~p mph.~n",
[Planemo, Distance, MphVelocity]),
     convert(Drop)
 end.

If you run this code, and feed it bad data, the convert/1 method will report an error message (mostly duplicating the shell) before exiting neatly.

1> c(mph_drop).
{ok,mph_drop}
2> Pid1=spawn(mph_drop,mph_drop,[]).
<0.45.0>
3> Pid1 ! {moon,20}.
On moon, a fall of 20 meters yields a velocity of 17.89549032 mph.
{moon,20}
4> Pid1 ! {moon,zoids}.
FAILURE: <0.46.0> died because of {badarith,
                                   [{drop,fall_velocity,2,
                                     [{file,"drop.erl"},{line,12}]},
                                    {drop,drop,0,
                                     [{file,"drop.erl"},{line,7}]}]}.

=ERROR REPORT==== 19-Dec-2016::21:13:46 ===
Error in process <0.46.0> with exit value: {badarith,[{drop,fall_velocity,2,
    [{file,"drop.erl"},{line,12}]},{drop,drop,0,[{file,"drop.erl"},{line,7}]}]}

{moon,zoids}

A more robust alternative would set up a new Drop variable, spawning a new process. That version, shown in Example 8-10, which you can find at ch08/ex10-resilient, is much tougher. Its receive clause sweeps away failure, soldiering on with a new copy (NewDrop) of the drop calculator if needed.

Example 8-10. Trapping a failure, reporting an error, and setting up a new process
-module(mph_drop).
-export([mph_drop/0]).

mph_drop() ->
  process_flag(trap_exit, true),
  Drop=spawn_link(drop,drop,[]),
  convert(Drop).

convert(Drop) ->
 receive
   {Planemo, Distance} ->
     Drop ! {self(), Planemo, Distance},
     convert(Drop);
   {'EXIT', _Pid, _Reason} ->
     NewDrop=spawn_link(drop,drop,[]),
     convert(NewDrop);
   {Planemo, Distance, Velocity} ->
     MphVelocity= 2.23693629 * Velocity,
     io:format("On ~p, a fall of ~p meters yields a velocity of ~p mph.~n",
 [Planemo, Distance, MphVelocity]),
     convert(Drop)
 end.

If you compile and run Example 8-10, you’ll see something like Figure 8-9 when you start Observer, go to Processes, and sort by Pid. If you feed it bad data, as shown on line 6 in the following code sample, you’ll still get the error message from the shell, but the process will work just fine. As Observer shows in Figure 8-10, it started up a new process to handle the drop:drop/0 calculations, and as line 8 shows, it works like its predecessor:

1> c(drop).
{ok,drop}
2> c(mph_drop).
{ok,mph_drop}
3> observer:start().
ok
4> Pid1=spawn(mph_drop,mph_drop,[]).
<0.4294.0>
5> Pid1 ! {moon,20}.
On moon, a fall of 20 meters yields a velocity of 17.89549032 mph.
{moon,20}
6> Pid1 ! {mars,20}.
On mars, a fall of 20 meters yields a velocity of 27.250254686571544 mph.
{mars,20}
7> Pid1 ! {mars,zoids}.
{mars,zoids}
8>
=ERROR REPORT==== 19-Dec-2016::21:18:38 ===
Error in process <0.4295.0> with exit value:
{badarith,[{drop,fall_velocity,2,[{file,"drop.erl"},{line,13}]},
           {drop,drop,0,[{file,"drop.erl"},{line,7}]}]}
Pid1 ! {moon,20}.
On moon, a fall of 20 meters yields a velocity of 17.89549032 mph.
{moon,20}
ier2 0809
Figure 8-9. Processes before an error (note the Pid for drop:drop/0)
ier2 0810
Figure 8-10. Processes after an error (note the change in Pid for drop:drop/0)

Erlang offers many more process management options. You can remove a link with unlink/1, or establish a connection for just watching a process with erlang:monitor/2. If you want to terminate a process, you can use exit/1 within that process, or exit/2 to specify a process and reason from another process.

Building applications that can tolerate failure and restore their functionality is at the core of robust Erlang programming. Developing in that style is probably a larger leap for most programmers than Erlang’s shift to functional programming, but it’s where the true power of Erlang becomes obvious.

Note

You can learn more about working with simple processes in Chapter 4 of Erlang Programming (O’Reilly); Chapter 12 of Programming Erlang, 2nd Edition (Pragmatic); Section 2.13 of Erlang and OTP in Action (Manning); and Chapters 10 and 11 of Learn You Some Erlang For Great Good! (No Starch Press).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset