Managing State with Processes

Functional programs are stateless, but we still need to be able to manage state. In Elixir, we use concurrent processes and recursion to handle this task. That may sound counterintuitive, but let’s take a look at how it works with a simple program.

To start with, let’s create a child application. From the rumbl_umbrella root directory, change to apps and create a new mix project, like this:

 $ ​​​​ ​​cd​​ ​​apps
 $ ​​​​ ​​mix​​ ​​new​​ ​​info_sys​​ ​​--sup
 * creating README.md
 ...
 $ ​​​​ ​​cd​​ ​​info_sys

We create a brand new mix project under the apps directory. Later it will evolve into our full service, but for now let’s create a Counter server that counts up or down. Create a apps/info_sys/lib/info_sys/counter.ex file and key this in:

1: defmodule​ InfoSys.Counter ​do
def​ inc(pid), ​do​: send(pid, ​:inc​)
def​ dec(pid), ​do​: send(pid, ​:dec​)
5: 
def​ val(pid, timeout \ 5000) ​do
ref = make_ref()
send(pid, {​:val​, self(), ref})
10: receive​ ​do
{^ref, val} -> val
after
timeout -> ​exit​(​:timeout​)
end
15: end
def​ start_link(initial_val) ​do
{​:ok​, spawn_link(​fn​ -> listen(initial_val) ​end​)}
end
20: 
defp​ listen(val) ​do
receive​ ​do
:inc​ ->
listen(val + 1)
25: 
:dec​ ->
listen(val - 1)
{​:val​, sender, ref} ->
30:  send(sender, {ref, val})
listen(val)
end
end
end

Our module implements a Counter server as well as functions for interacting with it as a client. The client serves as the API and exists only to send messages to the process that does the work. It’s the interface for our counter. The server is a process that recursively loops, processing a message and sending updated state to itself. Our server is the implementation.

Building the Counter API

Our API sends messages to increment (:inc) and decrement (:dec) the counter, and another message called :val to get the counter’s value. Let’s look at each one of these in turn.

:inc and :dec take only the process ID for the server process—called pid for process ID—and a single atom command. These skinny functions exist only to send :inc and :dec messages to our server process. These are asynchronous, meaning we send a message without awaiting any reply.

The val function on line 6 is a bit different. It must send a request for the value of the counter and await the response. Since we need to associate a response with this particular request, we create a unique reference with make_ref(). This unique reference is just a value that’s guaranteed to be globally unique. Then, we send a message to our counter with the send function. Our message payload is a 3-tuple with an atom designating the command we want to do, :val, followed by our process ID called pid and the globally unique reference.

Then, we await a response, matching on the reference. The ^ operator means that rather than rebinding the value of ref, we match only tuples having that exact ref. That way, we can make sure to match only responses related to our explicit request. If there’s no match in a given period, we exit the current process with the :timeout reason code.

We start by defining the client API to interact with our counter. First, we create inc and dec functions to increment and decrement our counter. These functions fire off an async message to the counter process without waiting for a response. Our val function sends a message to the counter but then blocks the caller process while waiting for a response.

Let’s take a look at our server.

As you’ll see later, OTP requires a start_link function. Ours, on line 17, accepts the initial state of our counter. Its only job is to spawn a process and return {:ok, pid}, where pid identifies the spawned process. The spawned process calls the private function named listen, which listens for messages and processes them.

Let’s look at that listen function on line 21, the engine for our counter. You don’t see any global variables that hold state, but our listener has a trick up its sleeve. We can exploit recursion to manage state. For each call to listen, the tiny function blocks to wait for a message. Then, we process the trivial :inc, :dec, and :val messages. The last thing any receive clause does is call listen again with the updated state.

Said another way, the state of the server is wrapped up in the execution of the recursive function. We can use Elixir’s message passing to listen in on the process to find the value of the state at any time. When the last thing you do in a function is to call the function itself, the function is tail recursive, meaning it optimizes to a loop instead of a function call. That means this loop can run indefinitely! In many languages, burning a thread for such a trivial task can be expensive, but in Elixir processes are incredibly cheap, so this strategy is a great way to manage state.

Taking Our Counter for a Spin

This code is pretty simple, so you already know what’ll happen. Still, let’s try it out in IEx with iex -S mix:

 iex>​ alias InfoSys.Counter
 InfoSys.Counter
 
 iex>​ {​:ok​, counter} = Counter.start_link(0)
 {:ok, #PID<0.253.0>}
 
 iex>​ Counter.inc(counter)
 :inc
 iex>​ Counter.inc(counter)
 :inc
 iex>​ Counter.val(counter)
 2
 
 iex>​ Counter.dec(counter)
 :dec
 iex>​ Counter.val(counter)
 1

It works perfectly, just as you expected. Think about the techniques used:

  • We used concurrency and recursion to maintain state.
  • We separated the interface from the implementation.
  • We used different abstractions for asynchronous and synchronous communication with our server.

As you might imagine, this approach is common and important enough for us to package it for reuse. In fact, this approach has been around a while in the form of the Erlang OTP library. Let’s take a look.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.166.31