Exploring Elixir processes

Expanding on our FizzBuzz example from the previous post.


Almost 3 months have passed since the release of Elixir 1.0, but we are still very excited about the language.

In this blog post we’ll expand on the FizzBuzz example from the previous post, which we’ll now turn into a server.

The FizzBuzz server
defmodule FizzBuzz do
  @moduledoc "A very simple FizzBuzz server"

  @doc """
    Computes and outputs the FizzBuzz value for the
    provided argument.
  """
  @spec compute(Integer) :: :ok
  def compute do
    receive do
      n -> IO.puts "#{n}: #{compute(n)}"
    end
    compute
  end

  defp compute(n) do
    case {rem(n, 3), rem(n, 5)} do
      {0, 0} -> :FizzBuzz
      {0, _} -> :Fizz
      {_, 0} -> :Buzz
      _      -> n
    end
  end
end

This is a simple module, with some documentation and a typespec for compute/0, which works as the main entry point for the FizzBuzz module. It uses receive/1 to get the next number from the process’ mailbox, computes (via the private compute/1 function) and outputs the result, before calling itself again to wait for the next message.

Communicating with the server

To use our newly created server, we first start it with spawn/3, which expects a module name, the name of the function to be called as well as that function’s arguments. It returns the pid of the newly created process, which is stored in the variable fb (remember that parenthesis around function arguments are optional in Elixir).
fb = spawn FizzBuzz, :compute, []

We then can use send/2 to send a message to the server:
send fb, 3

This will print “3: Fizz” and return 3. Now let’s send several messages to make sure the recursive call in compute/0 works as expected:
10..15 |> Enum.map(&send(fb, &1))

Here we use Elixir’s pipe operator (|>/2) to send the Range of numbers from 10 to 15 into Enum.map/2, which uses a partially applied version of send/2 with an anonymous argument (&1). The output is as expected:
10: Buzz
11: 11
12: Fizz
13: 13
14: 14
15: FizzBuzz

Dealing with errors

At this point the more observant readers may wonder what happens if instead of an integer, we send some other data type to FizzBuzz.compute/0. Let’s try:
send fb, 1.0

This leads to the following error message:
14:25:32.742 [error] Error in process <0.95.0> with exit value: {badarith,[{'Elixir.FizzBuzz',compute,1,[{file,"fizzbuzz.ex"},{line,10}]},{'Elixir.FizzBuzz',compute,0,[{file,"fizzbuzz.ex"},{line,4}]}]}

This means that the FizzBuzz server crashed. However, since we were dealing with an unlinked process, our main program (in this specific case an iex session) is unaffected by this. Now let’s start the server again, but this time as a linked process using spawn_link/3:
fb = spawn_link FizzBuzz, :compute, []

Now a problems in our FizzBuzz server will bubble up and also terminate the main process:
14:33:40.426 [error] Error in process <0.119.0> with exit value: {badarith,[{'Elixir.FizzBuzz',compute,1,[{file,"fizzbuzz.ex"},{line,10}]},{'Elixir.FizzBuzz',compute,0,[{file,"fizzbuzz.ex"},{line,4}]}]}


** (EXIT from #PID<0.116.0>) an exception was raised:
** (ArithmeticError) bad argument in arithmetic expression
fizzbuzz.ex:10: FizzBuzz.compute/1
fizzbuzz.ex:4: FizzBuzz.compute/0

This could be rectified by adding a guard clause to compute/0 in our FizzBuzz model like this:
def compute do
  receive do
    n when is_integer(n) -> IO.puts "#{n}: #{compute(n)}"
    _ -> IO.puts "Invalid argument"
  end
  compute
end

While this works, it goes against one of Erlang’s most important principles, “let it crash”. Instead of programming defensively, errors will just terminate a process and leave it up to monitoring processes (if there are any) to deal with this situation.

Monitoring

One way to deal with the above is to set up a monitor. Unlike a link this is a unidirectional relationship between processes. To create a monitor, we start the FizzBuzz server with spawn_monitor/3, which on top of the pid will also return a monitoring reference. We are not using this reference in the following example, but it’s useful for manually disabling monitoring with a call to demonitor/2.

Now in case the server dies, a message will be sent to the monitoring process:
{fb, ref} = spawn_monitor FizzBuzz, :compute, []
send fb, 1.0

Since we didn’t use receive/1 to set up a message receiver in our iex session, we will have to manually call the flush/0 helper to see the monitoring alert:
{:DOWN, #Reference<0.0.0.262>, :process, #PID<0.79.0>,
  {:badarith,
    [{FizzBuzz, :compute, 1, [file: 'fizzbuzz.ex', line: 16]},
    {FizzBuzz, :compute, 0, [file: 'fizzbuzz.ex', line: 10]}]}}

What’s next?

Dealing with processes in Elixir is very easy. However, by implementing the FizzBuzz server from scratch, we are not making full use of the power that the Erlang/OTP underpinnings provide to Elixir. So in the next installment of this series we’ll revisit the FizzBuzz server, but this time making use of the gen_server and supervisor OTP behaviors. Stay tuned!
Like 235 likes
Michael Kohl
Share:

Join the conversation

This will be shown public
All comments are moderated

Get our stories delivered

From us to your inbox weekly.