Playing with processes in Elixir

Elixir’s key organizational concept, the process, is an independent component built from functions that sends and receives messages.

By Simon St. Laurent and J. Eisenberg
January 31, 2017
Mill Mill (source: succo)

Elixir is a functional language, but Elixir programs are rarely structured around simple functions. Instead, Elixir’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.

Taking advantage of those features, though, 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.

Learn faster. Dig deeper. See farther.

Join the O'Reilly online learning platform. Get a free trial today and find answers on the fly, or master something new and useful.

Learn more

The Shell Is a Process

You’ve been working within a single process throughout this book so far, the Elixir 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, so in the shell you can just try the self() function:

iex(1)> self()
#PID<0.26.0>

#PID<0.26.0> is the shell’s representation of a process identifier; the three integers constitute a triple that provides a unique identifier for this process. You may get a different set of numbers when you try it. This group of numbers is guaranteed to be unique within this run of Elixir, not permanently the same in future use. Elixir uses pids internally, but while you can read them in the shell, you can’t type pids directly into the shell or into functions. Elixir much prefers that you treat pids as abstractions.

Note

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

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 gets time to check its mailbox, it will be able to retrieve and process the messages there.

Elixir, 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. You need to make sure your processes are assembled correctly.

The syntax for sending a message is pretty simple. You use the send/2 function with two arguments, an expression containing the pid and the message:

iex(2)> send(self(), :test1)
:test1
iex(3)> pid = self()
#PID<0.26.0>
iex(4)> send(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 send/2 function 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, but 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:

iex(5)> flush()
:test1
:test2
:ok
iex(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. 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:

iex(7)> send(self(), :test1)
:test1
iex(8)> receive do
...(8)>   x -> x
...(8)> end
:test1
iex(9)> send(self(), 23)
23
iex(10)> receive do
...(10)>   y -> 2 * y
...(10)> end
46

So far, so good. However, if you screw up—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 (in technical terms, receive blocks until it receives a message), 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 IEx. (x and y become bound variables, and even though they are not immutable, it is considered in the spirit of functional programming to not reuse them.)

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 1-1, which is in ch09/ex1-simple, shows a simple—excessively simple—module containing a function that reports messages it receives.

Example 1-1. An overly simple process definition
defmodule Bounce do
  def report do
    receive do
      msg -> IO.puts("Received #{msg}")
    end
  end
end

When the report/0 function receives a message, it will report that it received it. Setting this up means compiling the module and then using the spawn/3 function, which turns the function into a freestanding process. The arguments are the module name, the function name (as an atom), 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. The spawn/3 function will return the pid, which you should capture in a variable (here, pid):

iex(1)> pid = spawn(Bounce, :report, [])
#PID<0.43.0>

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

iex(2)> send(pid, 23)
Received 23
23

However, there’s one small problem. The report process exited—it went through the receive clause only once, and when it was done, it was done. If you try to send the process another message, you’ll get back the message, 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:

iex(3)> send(pid, 23)
23

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

Example 1-2. A function that creates a stable process
defmodule Bounce do
  def report do
    receive do
      msg -> IO.puts("Received #{msg}")
      report()
    end
  end
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 multiple messages to the process, as shown here:

iex(4)> r(Bounce)
warning: redefining module Bounce (current version loaded from
  _build/dev/lib/bounce/ebin/Elixir.Bounce.beam)
  /Users/elixir/code/ch09/ex2-recursion/lib/bounce.ex:1

{:reloaded, Bounce, [Bounce]}
iex(5)> pid2 = spawn(Bounce, :report, [])
#PID<0.43.0>
iex(6)> send(pid2, 23)
Received 23
23
iex(7)> send(pid2, :message)
Received message
:message
Note

Because processes are asynchronous, the output from send/2 may appear before the output from report/0.

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 1-3 shows the addition of an argument, in this case just an integer that gets incremented with each call. You can find it in ch09/ex3-counter.

Example 1-3. A function that adds a counter to its message reporting
defmodule Bounce do
  def report(count) do
    receive do
      msg -> IO.puts("Received #{count}: #{msg}")
      report(count + 1)
    end
  end
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:

$ iex -S mix
Erlang/OTP 19 [erts-8.0] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe]
  [kernel-poll:false]

Compiling 1 file (.ex)
Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> pid2 = spawn(Bounce, :report, [1])
#PID<0.43.0>
iex(2)> send(pid2, :test)
:test
Received 1: test
iex(3)> send(pid2, :test2)
:test2
Received 2: test2
iex(4)> send(pid2, :another)
:another
Received 3: another

Whatever you do in your recursive call, keeping it simple (and preferably tail recursive) is best, as these functions 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 1-4, in ch09/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 1-4. Using the return value of the receive clause as state for the next iteration
defmodule Bounce do
  def report(count) do
    new_count = receive do
      msg -> IO.puts("Received #{count}: #{msg}")
      count + 1
    end
    report(new_count)
  end
end

In this model, all (though there’s 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 Elixir expects an application to be a group of easily spawned processes? That run recursively?

Yes, absolutely. Elixir was written specifically to support that model, and its processes are more lightweight than those of pretty much any of its competitors. The Erlang scheduler that Elixir uses gets processes started and distributes processing time among them, as well as splitting 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. You don’t, though, 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. Elixir 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, just use the Process.register/2 built-in function. The first argument is the pid of the process, and the second argument is an atom, effectively the name you’re assigning the process. Once you have it registered, you can send it messages using the atom instead of a pid:

iex(1)> pid1 = spawn(Bounce, :report, [1])
#PID<0.39.0>
iex(2)> Process.register(pid1, :bounce)
true
iex(3)> send(:bounce, :hello)
:hello
Received 1: hello
iex(4)> send(:bounce, "Really?")
Received 2: Really?
"Really?"

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

iex(5)> send(:zingo, :test)
** (ArgumentError) argument error
    :erlang.send(: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 reregister it.

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

iex(5)> get_bounce = Process.whereis(:bounce)
#PID<0.39.0>
iex(6)> Process.unregister(:bounce)
true
iex(7)> test_bounce = Process.whereis(:bounce)
nil
iex(8)> send(get_bounce, "Still there?")
Received 3: Still there?
"Still there?"
Note

If you want to see which processes are registered, you can use the Process.registered/0 function.

If you’ve worked in other programming languages and learned the gospel of “no global variables,” you may be wondering why Elixir permits a systemwide list of processes like this. Most of the rest 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 1-5, in ch09/ex5-division, shows a report/0 function that can break if it gets input that isn’t a number.

Example 1-5. A fragile function
defmodule Bounce do
  def report do
    receive do
      x -> IO.puts("Divided to #{x / 2}")
      report()
    end
  end
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:

iex(1)> pid3 = spawn(Bounce, :report, [])
#PID<0.50.0>
iex(2)> send(pid3, 38)
38
Divided to 19.0
iex(3)> send(pid3, 27.56)
Divided to 13.78
27.56
iex(4)> send(pid3, :seven)
:seven
iex(5)>
14:18:59.471 [error] Process #PID<0.65.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    bounce.ex:4: Bounce.report/0
iex(5)> send(pid3, 14)
14

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

Processes Talking Amongst Themselves

Sending messages to Elixir 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 1-6, in ch09/ex6-talking, builds on the Drop module from Example 3-2, adding a drop/0 function that receives messages and making the fall_velocity/2 function private.

Example 1-6. A process that sends a message back to the process that called it
defmodule Drop do
  def drop do
    receive do
      {from, planemo, distance} ->
        send(from, {planemo, distance, fall_velocity(planemo, distance)})
        drop()
    end
  end

  defp fall_velocity(:earth, distance) when distance >= 0 do
    :math.sqrt(2 * 9.8 * distance)
  end

  defp fall_velocity(:moon, distance) when distance >= 0 do
    :math.sqrt(2 * 1.6 * distance)
  end

  defp fall_velocity(:mars, distance) when distance >= 0 do
    :math.sqrt(2 * 3.71 * distance)
  end
end

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

iex(1)> pid1 = spawn(Drop, :drop, [])
#PID<0.43.0>
iex(2)> send(pid1, {self(), :moon, 20})
{#PID<0.26.0>,:moon,20}
iex(3)> flush()
{:moon,20,8.0}
:ok

Example 1-7, which you’ll find in ch09/ex7-talkingProcs, shows a process that calls that process, just to demonstrate that this can work with more than just the shell. We use IO.write/1 so that the code listing doesn’t stretch off the page, but the output will all appear on one line.

Example 1-7. Calling a process from a process, and reporting the results
defmodule MphDrop do
  def mph_drop do
    drop_pid = spawn(Drop, :drop, [])
    convert(drop_pid)
  end

  def convert(drop_pid) do
    receive do
      {planemo, distance} ->
        send(drop_pid, {self(), planemo, distance})
        convert(drop_pid)
      {planemo, distance, velocity} ->
        mph_velocity = 2.23693629 * velocity
        IO.write("On #{planemo}, a fall of #{distance} meters ")
        IO.puts("yields a velocity of #{mph_velocity} mph.")
        convert(drop_pid)
    end
  end
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 1-6, and stores the pid in drop_pid. Then it calls convert/1, which will listen for messages recursively.

Note

If you don’t separate the initialization from the recursive listener, your code will work, but it 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. (As your code grows more complex, you will likely want to use more explicit flags about the kind of information contained in a message.) When the receive clause gets a message with two arguments, it sends a message to drop_pid, identifying itself as the sender and passing on the arguments. When the drop_pid process 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 it makes the velocity more intelligible to Americans.)

Using this from the shell looks like the following after invoking iex -S mix:

iex(1)> pid1 = spawn(MphDrop, :mph_drop, [])
#PID<0.47.0>
iex(2)> send(pid1, {:earth, 20})
On earth, a fall of 20 meters
yields a velocity of 44.289078952755766 mph.
{:earth,20}
iex(3)> send(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 MphDrop.convert/1. That process sent a message to drop_pid, the process identifier for Drop.drop/0, which MphDrop.mph_drop/0 set up when it was spawned. That process returned another message to MphDrop.convert/1, which reported to standard output (in this case, the shell). Those messages passed and were processed rapidly, but 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. Observer, a tool that lets you observe and manage processes, offers a GUI that lets you look into the current state of your processes and see what’s happening. Depending on how you installed Erlang when you installed Elixir, you may be able to start it from a toolbar, but you can always start it from the shell:

iex(4)> :observer.start
#PID<0.49.0>
Warning

In order to use Observer, you need wxwidgets installed and Erlang compiled to support it.

When you click the Processes tab you’ll see something like Figure 1-1 appear. It’s a long list of processes, more than you probably wanted to know about. If you click the Current Function column header twice to sort the list in reverse order, you will see the Elixir processes at the top, similar to Figure 1-2.

inel 09 processes
Figure 1-1. Observer’s process list when first loaded
inel 09 sorted
Figure 1-2. Observer’s process list after sorting by Current Function

Observer will update the process list every 10 seconds. If you would prefer to control the refresh yourself, choose Refresh Interval from the View menu, and uncheck Periodical Refresh.

Watching Messages Among Processes

The list of processes is useful, but Observer also lets you look inside of process activity. This is a slightly more complex process, so take a deep breath!

  1. Find the Elixir.MphDrop:mph_drop/0 process and right-click it.

  2. Choose “Trace selected processes by name (all nodes)” and select all items in the left of the dialog, as shown in Figure 1-3. Then click OK.

  3. Click the Trace Overview tab.

  4. Click Start Trace, and you will get a warning message as shown in Figure 1-4. You may safely ignore that message.

inel 09 trace
Figure 1-3. Options in trace processes
inel 09 start trace
Figure 1-4. Starting a trace

This will open up a new window, which may display a message like “Dropped 10 messages.” Now make the process do something:

iex(5)> send(pid1, {:mars, 20})
On mars, a fall of 20 meters
yields a velocity of 27.25025468657154448238 mph.
{:mars,20}

The Observer window for that process will update to show messages and calls, as shown in Figure 1-5. << means a message was received, whereas ! indicates a message sent.

inel 09 trace log
Figure 1-5. Tracing calls when you send mph_drop a message

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), with the message landing in the mailbox but not triggering 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 Drop.drop/0, it doesn’t make much sense to leave the MphDrop.convert/1 process hanging around. You can see how this works through the shell and Observer. First, start up Observer, go to the process window, and then, from the command line, spawn MphDrop.mph_drop/0:

iex(1)> :observer.start()
:ok
iex(2)> pid1 = spawn(MphDrop, :mph_drop, [])
#PID<0.82.0>

You’ll see something like Figure 1-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 1-7:

iex(3)> send(pid1, {:moon, :zoids})

19:28:27.825 [error] Process #PID<0.83.0> raised an exception                                                               
** (ArithmeticError) bad argument in arithmetic expression
    (mph_drop) lib/drop.ex:15: Drop.fall_velocity/2
    (mph_drop) lib/drop.ex:5: Drop.drop/0
inel 09 pre error
Figure 1-6. A healthy set of processes
inel 09 post error
Figure 1-7. Only the drop:drop/0 process is gone

Since the remaining MphDrop.convert/1 process is now useless, it would be better for it to halt when Drop.drop/0 fails. Elixir lets you specify that dependency with a link. The easy way to do that while avoiding potential race conditions is to use spawn_link/3 instead of just spawn/3. Everything else in the module remains the same. This is shown in Example 1-8, which you can find in ch09/ex8-linking.

Example 1-8. Calling a linked process from a process so failures propagate
defmodule MphDrop do
  def mph_drop do
    drop_pid = spawn_link(Drop, :drop, [])
    convert(drop_pid)
  end

  def convert(drop_pid) do
    receive do
      {planemo, distance} ->
        send(drop_pid, {self(), planemo, distance})
        convert(drop_pid)
      {planemo, distance, velocity} ->
        mph_velocity = 2.23693629 * velocity
        IO.write("On #{planemo}, a fall of #{distance} meters ")
        IO.puts("yields a velocity of #{mph_velocity} mph.")
        convert(drop_pid)
    end
  end
end

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 1-8:

iex(1)> :observer.start()
:ok
iex(2)> pid1 = spawn(MphDrop, :mph_drop, [])
#PID<0.162.0>
iex(3)> send(pid1, {:moon, :zoids})
{:moon,:zoids}
iex(4)>

19:30:26.822 [error] Process #PID<0.163.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    (mph_drop) lib/drop.ex:15: Drop.fall_velocity/2
    (mph_drop) lib/drop.ex:5: Drop.drop/0
inel 09 linked processes
Figure 1-8. Both processes now depart when there is an error
Note

Links are bidirectional. If you kill the the MphDrop.mph_drop/0 process—with, for example, Process.exit(pid1,:kill).—the Drop.drop/0 process will also vanish. (:kill is the harshest reason for an exit, and isn’t trappable 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 Elixir processes, and makes sense in many contexts, but you can also have a process trap exits. When an Elixir process fails, it sends an explanation, in the form of a tuple, to other processes that are linked to it. 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 Pro⁠cess​.flag(:trap_exit, true), these error reports arrive as messages, rather than just killing your process.

Example 1-9, in ch09/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 which will listen for exits and report them more neatly.

Example 1-9. Trapping a failure, reporting an error, and exiting
defmodule MphDrop do
  def mph_drop do
    Process.flag(:trap_exit, true)
    drop_pid = spawn_link(Drop, :drop, [])
    convert(drop_pid)
  end

  def convert(drop_pid) do
    receive do
      {planemo, distance} ->
        send(drop_pid, {self(), planemo, distance})
        convert(drop_pid)
      {:EXIT, pid, reason} ->
        IO.puts("Failure: #{inspect(pid)} #{inspect(reason)}")
      {planemo, distance, velocity} ->
        mph_velocity = 2.23693629 * velocity
        IO.write("On #{planemo}, a fall of #{distance} meters ")
        IO.puts("yields a velocity of #{mph_velocity} mph.")
        convert(drop_pid)
    end
  end
end

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

iex(1)> pid1 = spawn(MphDrop, :mph_drop, [])
#PID<0.144.0>
iex(2)> send(pid1, {:moon, 20})
On moon, a fall of 20 meters
yields a velocity of 17.89549032 mph.
{:moon,20}
iex(3)> send(pid1, {:moon, :zoids})
Failure: #PID<0.145.0> {:badarith, [{Drop, :fall_velocity, 2, 
  [file: 'lib/drop.ex', line: 15]}, 
  {Drop, :drop, 0, [file: 'lib/drop.ex', line: 5]}]}
  {:moon, :zoids}
iex(4)> 
12:04:31.360 [error] Process #PID<0.145.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    (mph_drop) lib/drop.ex:15: Drop.fall_velocity/2
    (mph_drop) lib/drop.ex:5: Drop.drop/0
    
    nil
iex(5)>

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

Example 1-10. Trapping a failure, reporting an error, and setting up a new process
defmodule MphDrop do
  def mph_drop do
    Process.flag(:trap_exit, true)
    drop_pid = spawn_link(Drop, :drop, [])
    convert(drop_pid)
  end

  def convert(drop_pid) do
    receive do
      {planemo, distance} ->
        send(drop_pid, {self(), planemo, distance})
        convert(drop_pid)
      {:EXIT, _pid, _reason} ->
        new_drop_pid = spawn_link(Drop, :drop, [])
        convert(new_drop_pid)
      {planemo, distance, velocity} ->
        mph_velocity = 2.23693629 * velocity
        IO.write("On #{planemo}, a fall of #{distance} meters ")
        IO.puts("yields a velocity of #{mph_velocity} mph.")
        convert(drop_pid)
    end
  end
end

If you compile and run Example 1-10, you’ll see Figure 1-9 when you first start Observer. 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 you’ll see in Observer, as shown in Figure 1-10, it started up a new process to handle the Drop.drop/0 calculations, and as line 7 shows, it works like its predecessor:

iex(1)> pid1 = spawn(MphDrop, :mph_drop, [])
#PID<0.145.0>
iex(2)> :observer.start()
:ok
iex(3)> send(pid1, {:moon, 20})
On moon, a fall of 20 meters
yields a velocity of 17.89549032 mph.
{:moon,20}
iex(4)> send(pid1, {:mars, 20})
On mars, a fall of 20 meters
yields a velocity of 27.250254686571544 mph.
{:mars, 20}
iex(5)> send(pid1, {:mars, :zoids})
{:mars, :zoids}
Failure: #PID<0.109.0> {:badarith, [{Drop, :fall_velocity, 2,
  [file: 'lib/drop.ex', line: 19]},
  {Drop, :drop, 0, [file: 'lib/drop.ex', line: 5]}]}
iex(6)> 
15:59:49.713 [error] Process #PID<0.146.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    (mph_drop) lib/drop.ex:19: Drop.fall_velocity/2
    (mph_drop) lib/drop.ex:5: Drop.drop/0

nil  
iex(7)> send(pid1, {:moon, 20})
On moon, a fall of 20 meters
yields a velocity of 17.89549032 mph.
{:moon,20}
top line PID 0.2501.0
Figure 1-9. Processes before an error—note the Pid on the top line
top line PID 0.3990.0
Figure 1-10. Processes after an error—note the top line Pid change

Elixir offers many more process management options. You can remove a link with Process.unlink/1, or establish a connection for just watching a process with Process.monitor/1. If you want to terminate a process, use Process.exit/2 to specify a process and reason. You may specify another process’s pid or self().

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

Post topics: Software Engineering
Share: