Explaining Erlang, Part 2 – Sending Messages
February 10, 2017
erlang
In this post, we are going to look at exactly how we create processes and send messages to them. I’m also going to start introducing code, but only enough to demonstrate the core concepts. Most Elixir books I’ve read start by showing you the language, and only later start showing you how to build programs from processes. This makes sense to some extent because it’s easier to show code if the reader can read it.
However, I find that approach distracting because I’m being fed a lot of information without being able to apply it. So, in this post, I’ll start introducing the language, but only as much as I think you’ll need to understand how to build programs with processes.
Also of note: I’m going to show only Elixir syntax. While I like Joe Armstrong’s explanation for Erlang syntax in his book and I admire the principles behind it, I think Elixir will have more common ground with Rubyists and there’s a better chance the syntax will give them one less thing to think about as they read this.
That said, I think learning Erlang syntax is very valuable because:
- It’s another way to think about how one might write a language
- In my opinion, the best books written about the Erlang/OTP eco-system ask you to be able to read (but not write) Erlang. Those books are very valuable.
List:
- Programming Erlang
- Designing for Scalability with Erlang/OTP
- Learn You Some Erlang for Great Good
- Stuff Goes Bad: Erlang in Anger
This is Part 2 of my ongoing series of notes for a presentation I’m making for developers new to Erlang/Elixir. Part 1 is here.
Let’s look at some code.
-
Make a new project with mix and run the REPL inside it.
$ mix new preso ; cd preso ; iex -S mix
-
You should see:
iex(1)>
This is the REPL.
If you look at the file lib/preso.ex
, you’ll see this code. Wipe out the boilerplate comments for now.
defmodule Preso do
def hello do
:world
end
end
You’ll see we made a module (just like in Ruby) with one FUNCTION (not method) called hello
. If you call it, it will return an atom.
iex(1)> Preso.hello
:world
iex(2)>
Two things to note here. You are making modules and functions. There are no classes and methods. One other thing to note is that you are currently executing in the context of a process inside of iex
.
iex(2)> self()
#PID<0.136.0>
iex(3)>
self()
is a function in the Kernel
module which returns the Process Identifier (or “pid”) of the current process.
Let’s delete the hello
function and add one called loop
.
defmodule Preso do
def loop() do
receive do
message ->
IO.puts "Got message: #{inspect message}"
end
loop()
end
end
Notice two things. loop()
calls itself. Erlang implements tail-call elimination so this will not blow up your stack. But also look at the receive
statement. This means “I want to read the current processes' mailbox. message
represents the piece of data received in the message, ->
means “Do this next thing with message
”.
There is a bit more to this that I’m leaving out, but that’s what you need to know for now.
First, we are going to recompile this file. We do not need to leave iex
!
iex(3)> recompile()
:ok
iex(4)>
Now we are going to call our new function: Preso.loop()
. We are going to invoke this differently than before. If we just typed Preso.loop()
, the iex
process (which is the current process) would sit and wait to receive a message. To avoid that, we are going to call it from inside a new process. When you create a new process, you have to supply them with a starter function so we’ll start it with loop()
. We’ll use the spawn/3
call (the /3
refers to the number of arguments.
The spawn
function is defined as: spawn(Module, function, args)
and returns a pid for the new process.
iex(4)> pid = spawn(Preso, :loop, [])
#PID<0.177.0>
iex(5)>
Now that we have a pid, we can use the send/2
call to send it something. The send/2
function is defined as: send(pid, message)
where message can be anything. Since our loop()
function just prints out what it receives, we should see the output.
iex(5)> send(pid, "hello")
Got message: "hello"
"hello"
iex(6)>
You can see “hello” twice in the result. The first is the result of loop()
outputting the message it received. The second is because the send()
call returns the message it sent. Note that the only way we can tell that loop()
got it is because we coded it to have a side-effect which was writing to stdout.
It we want to make a call to our new process that returns us a result, we need to send it our pid so it can call back to us.
Let’s make a new module in the same file:
defmodule PresoClient do
def send_and_wait(pid, message) do
send(pid, {self(), message})
receive do
reply ->
IO.puts "Result: #{inspect result}"
end
end
end
In this new module, you’ll see that we send our message, just like we used to, but this time, we are sending as a message, a “tuple”. A tuple is a fixed-length list of values, inside {}
and separated by ,
. We are sending the tuple {self(), message}
which first has the the pid of our current process and then the message. By sending the current pid, the recipient can send a reply back. After the send()
, we wait to return from the send_and_wait/2
call until we receive a message back.
Unfortunately, our Preso.loop
isn’t written to send a reply so we’ll need to update it.
defmodule Preso do
def loop() do
receive do
- message ->
- IO.puts "Got message: #{inspect message}"
+ {from_pid, message} ->
+ send(from_pid, "Result: #{inspect message}")
end
loop()
end
end
Back to mix, let’s recompile, make a new pid with the new source (the old pid is still running the old), and use our new client function to call it.
iex(6)> recompile()
:ok
iex(7)> pid = spawn(Preso, :loop, [])
#PID<0.140.0>
"hello"
iex(8)> PresoClient.send_and_wait(pid, "hello")
Result: "Got message: \"hello\""
:ok
iex(9)>
From the output, you can see that our client method sent a message with the current process (iex
) and then waited until the current process (again iex
) received a reply from the recipient.
In this way, we’ve turned PresoClient.send_and_wait/2
into a synchronous call that returns a value from the recipient.
A little reorganization
Now, I had us put the send_and_wait/2
call into a separate module because I wanted to keep our server code clearly separated from the client code, but there is no reason they couldn’t be in the same module. All we’ve done is call functions after call, and there may be some advantages to keeping them in the same module as we’d have both the client API and the server code in one file. Let’s do this:
defmodule Preso do
# Client API
def send_and_wait(pid, message) do
send(pid, {self(), message})
receive do
reply ->
IO.puts "Result: #{inspect result}"
end
end
# Server code
def loop() do
receive do
{from_pid, message} ->
send(from_pid, "Result: #{inspect message}")
end
loop()
end
end
Lastly, let’s make a start/0
function so we don’t have to call spawn/3
ourselves.
defmodule Preso do
# Client API
def start do
spawn(Preso, :loop, [])
end
def send_and_wait(pid, message) do
send(pid, {self(), message})
receive do
reply ->
IO.puts "Result: #{inspect result}"
end
end
# Server code
def loop() do
receive do
{from_pid, message} ->
send(from_pid, "Result: #{inspect message}")
end
loop()
end
end
One more thing. Now that we have the start
method that returns a new process started from the loop/0
call, we don’t actually ever need to call loop
directly, and in fact, we probably only want it to be called from processes made from the start
call. To enforce this, we can actually make the loop/0
call private by changing def
to defp
.
defmodule Preso do
# Client API
def start do
spawn(Preso, :loop, [])
end
def send_and_wait(pid, message) do
send(pid, {self(), message})
receive do
reply ->
IO.puts "Result: #{inspect result}"
end
end
# Server code
- def loop() do
+ defp loop() do
receive do
{from_pid, message} ->
send(from_pid, "Result: #{inspect message}")
end
loop()
end
end
Now we have a module: Preso
that exposes a set of client functions to start a new process (based on loop
) and make a synchronous call to it (send_and_wait
).
We have a subtle bug in this code, but to fix it, we need to use make_ref()
and take advantage of a feature in Erlang called “pattern matching”. You’ve already used it, but didn’t know it {from_pid, message}
.
TODO … Explain pattern matching and the bug …
All we’ve done now is send messages between processes, but our process doesn’t hold any state. We’ll change that in the next part.