[001.4] Processes and Messaging

Processes and Messaging
[02.13.2017]

Concurrency is one of the primary draws to Elixir and Erlang. Concurrency is
achieved via Processes. Let's have a look at spawning processes and sending
messages between them.

Project

We'll start a new project:

mix new ping_pong
cd ping_pong

Now let's start out with a test:

vim test/ping_pong_test.exs

We'll test that we can spawn a process, send it a ping message, and get a
message back.

defmodulePingPongTestdouseExUnit.CasedoctestPingPongtest"it responds to a pong with a ping"do# spawn takes a module, a function name, and a list of arguments# It starts a new process, running that function. When the function# completes, the new process will die.ping=spawn(Ping,:start,[])# send lets you send messages to a process# self provides the current process's PID, or Process IDsend(ping,{:pong,self()})# We'll assert that when we send a pong to a process, we receive back a ping# This waits for up to 100ms and passes if the message is received in that# time frame, failing if it isn't.assert_receive{:ping,^ping}endend

OK, so we can try to run this:

mix test

It will fail because there's no Ping module with a start function. We can
make that module:

vim lib/ping.ex

defmodulePingdodefstartdo:okendend

If you run the test now, it will fail because it never receives a message back.
This is because our process doesn't respond to those messages, of course. To
listen for a message, we can use receive. We'll listen for a pong message,
capture the process id that we're sent, and send a ping message back.

defmodulePingdodefstartdo# receive pattern matches on a series of potential messages and runs some# code when it receives that message. Here we'll just send a message to the# pid we're sent.receivedo{:pong,from}->send(from,{:ping,self()})endendend

Now this works and we can send this message, and our tests pass. But if we were
to send another message, we would get no response. That's because after our ping
process has received a message, the function is complete and the process dies.
Let's write a test that we can send two messages:

test"it responds to two messages"doping=spawn(Ping,:start,[])send(ping,{:pong,self()})assert_receive{:ping,^ping}send(ping,{:pong,self()})assert_receive{:ping,^ping}end

Next, we will extract this function into a loop function and have loop call
itself via recursion:

Now, it will respond to the first message and then call loop again. If we run
the test, we'll see that it passes.

It's worth talking a little bit about how receive works. Processes have a
mailbox, and any messages sent to a process queue up in a list in the mailbox.
receive will look at the mailbox, and handle the first message it finds in
the order specified in the call to receive. If there are no messages, it
blocks until there is a message.

Grand Finale

For the grand finale, let's add one more message to this - we will make Ping
handle a :ping message as well, responding with :pong in that case. Then we
will output to the console each time a message is received, and we'll start two
processes talking to one another:

sign up for full access

Meet your expert

I've been building web-based software for businesses for over 18 years. In the last four years I realized that functional programming was in fact amazing, and have been pretty eager since then to help people build software better.