I haven’t been blogging about Elixir lately, and that’s only because I’ve been overwhelmed.
Coming up with new blog posts has been slightly challenging to say the least. Writing about Elixir’s features are great, but I really wanted to get down into the fundamentals: Erlang, OTP and designing Erlang/OTP programs.
I have since bought myself a couple more Erlang books that I can’t wait to get my hands on - Erlang Programming, Erlang and OTP in Action, and Handbook of Neuroevolution in Erlang.
It was Corey Haines’ endorsement that compelled me the buy Handbook of Neuroevolution in Erlang. (800 pages of awesome, that was how he vaguely put it.)
Introducing, Elixir By Example
Learning a new language is best done via experimentation. So I’m starting a new series - Elixir by Example.
A nice touch of Erlang Programming is the end of chapter exercises. Since I’m working on that book now, I will pick the interesting ones that I have attempted (and struggled with!) and share my solutions written in Elixir.
And without further ado …
Here’s the problem that we’ll be tackling today:
Write a function which starts N processes in a ring, and sends a message M times around all the processes in the ring. After the messages have been sent the processes should terminate gracefully.
And for the more visually-inclined:
(Source: http://www.erlang.org)
Here’s the skeleton of the program:
defmodule Ring do
def run(m, n, message) do
...
end
def start_process do
...
end
def loop(next_pid) do
receive do
...
end
end
end
The Loop
We begin with easy part, the loop
function:
def loop(next_pid) do
receive do
{:message, message, 0} ->
next_pid <- {:message, message, 0}
:ok
{:message, message, m} ->
next_pid <- {:message, message, m-1}
loop(next_pid)
end
end
There are two kinds of messages we would receive: m > 0
and m = 0
.
In the former case, we will send the same message, with m
decremented, then call loop
again so that the process is ready to receive the next message.
In the latter, we will forward the m == 0
message to the next process, which is our signal for the process to exit.
So how do we know which process to send the message to? An easy option would be to simply pass in the pid of the next message into the loop
function.
Now that we have settled on our loop
function, we can start thinking about setting up our process ring.
The Process Ring
The main difficulty in this problem is setting up the process ring. The approach I chose was for each process to spawn the next. But how do you link the last process back to the first?
I can think of at least 2 approaches. This post will talk about one of them. I will cover the next approach in a separate post.
Pattern matching is especially suited for thinking through all the cases. We have 3 to consider:
- First Process
- Intermediate Processes
- Last Process
Our problem would be that much easier if we have a way to link back the first process to the last, or vice versa. We cannot store state in Elixir/Erlang, as least not in the traditional sense. But, we definitely can pass state around via message passing and recursion.
The function start_process
will spawn one process, which would recursively spawn the next, until the count
is 0. Then, it runs loop
, so that each of the spawned process can begin handling messages.
As mentioned previously, there are 3 separate cases to handle.
1. The First Process
def start_process(count) do
pid = spawn_link(__MODULE__, :start_process, [count-1, self])
loop(pid)
end
The start_process/1
function takes the count
, spawns another process, passing in the decremented count
and it’s own pid. This pid belongs to the last process, and is also how we remember the pid of the last process.
2. The Intermediate Processes
The trick here is to realize that we can store the last pid, and use it when we are spawning the last process. For intermediate processes (All processes except the first and last spawned process) we simply pass the pid around using last
as an argument.
Otherwise, the function looks similar to the previous start_process/1
.
def start_process(count, last) do
pid = spawn_link(__MODULE__, :start_process, [count-1, last])
loop(pid)
end
3. The Last Process
When count
is 0, we know that we have spawned our last process. This time, we can finally make use of the pid (stored in last
) and link the first process to the last.
def start_process(0, last) do
loop(last)
end
Tying it all together
We need a main entry point. run
takes in all the necessary parameters, and then spawns the first process. By the time pid = spawn_link(__MODULE__, :start_process, [n-1])
completes, our ring would be set up.
To get things going, the initial message is then sent to the first process with pid <- {:message, message, m}
def run(m, n, message) do
pid = spawn_link(__MODULE__, :start_process, [n-1])
pid <- {:message, message, m}
end
Concluding Remarks
The full source code located here. I encourage you to play with it, and then try this exercise out for yourself.
This problem proved to be pretty difficult to me, and there a couple of stumbling roadblocks:
Process Termination
Making sure all the processes were terminated after m = 0
took a little thinking and a lot of guesswork. The only reason I noticed this was I fired up :pman.start
.
After running the program I noticed that the loop
processes were lingering around. See if you can replicate this effect by removing just one line in the program):
Ring Setup
I would be the first to admit that figuring this out independently proved too challenging and time consuming. Therefore I had to cheat a little and see how others approached the same problem.
I personally think you shouldn’t feel too bad about this, as long as you learn. (:
Thanks for reading!