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:

  1. It’s another way to think about how one might write a language
  2. 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:

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.

  1. Make a new project with mix and run the REPL inside it.

    $ mix new preso ; cd preso ; iex -S mix
    
  2. 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.

comments powered by Disqus