[002.2] Processes With State, and Agent

Managing state with processes and messaging, then seeing the Agent module from the Standard Library for a simpler way to do the same thing.

Subscribe now

Processes With State, and Agent [02.13.2017]

In the last episode I asked you to think about how you might implement mutable state using a process. Today we'll look at doing it ourselves, as well as checking out Agent from the Standard Library that exists for this very reason. Let's get started.

Project

We'll start a new project:

mix new stateful_processes
cd stateful_processes

We'd like to build a process that represents a counter. We can increment it or decrement it by sending it messages. This means it has to maintain an internal state that we can change. Elixir's an immutable language, so how do we manage this? Let's start out with some tests:

defmodule StatefulProcessesTest do
  use ExUnit.Case

  test "starting the counter" do
    {:ok, pid} = Counter.start(0)
    assert is_pid(pid)
  end
end

We'd like to start a counter, passing it an initial value. Let's make sure that works:

vim lib/counter.ex
defmodule Counter do
  def start(initial_value) do
    {:ok, spawn(Counter, :loop, [initial_value])}
  end

  def loop(value) do
    :ok
  end
end

Let's build out a means to get the value from this process, following the same pattern we used in the last episode:

defmodule StatefulProcessesTest do
  # ...
  test "getting the value" do
    {:ok, pid} = Counter.start(0)
    assert {:ok, 0} = Counter.get_value(pid)
  end

We'll receive a message in our loop and return the value we know about, and write a function that sends a message and receives the value back.

defmodule Counter do
  # ...
  def loop(value) do
    receive do
      # In our loop we'll expect to be told who a message is from, a ref that is
      # unique to their request, and a term that tells us what to do - in this
      # case, get the value of the counter.
      #
      # We'll send the value back with the ref so they know it's the appropriate
      # response, and then we'll tail-call with the existing value
      {from, ref, :get_value} ->
        send(from, {:ok, ref, value})
        loop(value)
    end
  end

  # Then we just make the nice function to interact with our process loop
  def get_value(pid) do
    ref = make_ref()
    send(pid, {self(), ref, :get_value})
    receive do
      {:ok, ^ref, val} -> {:ok, val}
    end
  end
end

With this, the tests pass. From here, we can add two new messages to our receive loop - :increment and :decrement. Let's write a test for increment:

  test "incrementing the value" do
    {:ok, pid} = Counter.start(0)
    :ok = Counter.increment(pid)
    assert {:ok, 1} = Counter.get_value(pid)
  end

This fails because there is no such function, of course. Let's add the function and the corresponding receive case:

defmodule Counter do
  # ...
  def loop(value) do
    receive do
      # ...
      :increment ->
        loop(value + 1)
    end
  end
  # ...
  def increment(pid) do
    send(pid, :increment)
    :ok
  end
end

Now this recursive function calls back into itself with a modified argument, in the event of an :increment message in the mailbox. This is how we can use processes to manage mutable state. If you run the tests, you'll find they pass. From here, it's easy to see how to implement decrement:

  test "decrementing the value" do
    {:ok, pid} = Counter.start(0)
    :ok = Counter.decrement(pid)
    assert {:ok, -1} = Counter.get_value(pid)
  end
defmodule Counter do
  # ...
  def loop(value) do
    receive do
      # ...
      :decrement ->
        loop(value - 1)
    end
  end
  # ...
  def decrement(pid) do
    send(pid, :decrement)
    :ok
  end
end

And the tests pass. So this is how you can implement mutable state with processes. Of course this is something that people frequently do, so we'll discuss GenServer next week.

For now, though, let's look at something else the standard library provides us: Agent. According to the documentation

Agents are a simple abstraction around state.

That is, they provide an easy way to wrap some state up in a process and interact with it. Let's look at how we could use an Agent in place of our hand-rolled process to build this counter:

defmodule Counter do
  def start(initial_value) do
    # Starting an agent just requires giving it a function that returns the
    # agent's initial value.
    Agent.start(fn -> initial_value end)
  end

  def get_value(pid) do
    # To get the value, we pass a function that receives the agent's state and
    # returns whatever we want - in our case, we want to wrap the value in an
    # `:ok` 2-tuple.
    Agent.get(pid, fn(x) -> {:ok, x} end)
  end

  def increment(pid) do
    # To increment, we send a function that tells the agent what to do with its
    # state.
    Agent.update(pid, fn(x) -> x + 1 end)
  end

  def decrement(pid) do
    # To decrement, we send a function that tells the agent what to do with its
    # state.
    Agent.update(pid, fn(x) -> x - 1 end)
  end
end

This is a bit nicer to read, clearly, but it works just as well as our hand-rolled process. I wanted to go through managing mutable state with processes by hand initially so that you could see that there's no magic here - just a nice API on top of essentially what we were doing before. It's not exactly the same, because it's build on top of OTP - we'll get into that a bit more next week.

Summary

In today's episode, we saw how to manage mutable state with processes. First, by rolling our own process. Then, by using the Agent module from the standard library, which exists for this exact purpose - introducing an actor that owns some mutable state.

I hope you enjoyed it. See you soon!

Resources