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!