Using Elixir Processes to Hold State
In the past two articles we’ve seen Elixir processes used to send and receive messages. Today I want to quickly look into another use for processes. That use is preserving state.
Because Elixir processes are so lightweight, it’s trivial to spawn one and pass some data off to it. Then you can retrieve that process and read or update the data you’ve stored there. It’s a cheap and isolated form of state mutability, without any worry of accidentally contaminating your code. And Elixir makes it just about as cheap as possible, via the Agent
module.
Agents
Starting
To start an agent, all it takes is a call to Agent.start/2
(or Agent.start_link/2
, if you’re building a supervision tree) and pass in a function defining your initial state, and an optional keyword list of arguments. Let say we’re implementing a basic counter starting at 0, so we’d start with something like this:
iex> {:ok, counter} = Agent.start(fn -> 0 end)
Successfully starting a process in Elixir returns a tuple of {status, pid}.
Querying
Now we can query our counter to make sure it’s correctly started at 0:
iex> Agent.get(counter, fn x -> x end)
0
Here we’re just asking for the value stored in our agent back without any modification. We can make this even shorter using Elixir’s anonymous function shorthand:
iex> Agent.get(counter, &(&1))
0
Updating
The corollary to get
is update
, and it works in much the same way, taking our agent as its first argument, and an updating function is its second. This function takes as its single argument the current state stored in our agent. For example, to increment our counter:
iex> Agent.update(counter, &(&1 + 1))
:ok
update
returns only the status of the function, so we would have to Agent.get
again to see our state.
iex> Agent.get(counter, &(&1))
1
Or would we?
We actually wouldn’t, because Agent
gives us the function get_and_update
, which takes the same arguments as update
, with one small difference. The function we pass as the second argument must return a two-element tuple, of the form {state_before_update, state_after_update}. The return value of the call to get_and_update
will the our state before the update, and the state after the update will now be stored in the agent.
iex> Agent.get_and_update(counter, fn x -> {x, x + 1} end)
2
iex> Agent.get(counter, &(&1))
3
You could also define the get_and_update
handler function using shorthand as &({&1, &1 + 1})
, but personally I find that starts to become harder to read quickly as the function becomes more complex. But that is purely a matter of taste, so do what you will.
It should be noted that neither update
nor get_and_update
need to perform any operations on the current value of the agent. For example, we could easily do
iex> Agent.update(n, fn _ -> “Hello” end)
This would rather spoil our expectation of n
as a counter, but it’s not illegal.
To wrap up, how about a brief real-world example?
Well, how about it?
Ok, great. Let’s say you’re building a testing framework in Elixir, because you kind of miss RSpec from Ruby but haven’t yet discovered the amazing ESpec. So you need a way to store the count of passed and failed specs as your tests run, but you don’t want to pollute your code by having to pass a map or keyword list around. You might write something like this:
defmodule Tester do
def start_run do
Agent.start(fn -> [total: 0, passed: 0, failed: 0] end, name: :test_stats)
end
def record_pass do
Agent.update(:test_stats, fn [total: t, passed: p, failed: f] ->
[total: t + 1, passed: p + 1, failed: f]
end)
end
def record_fail do
Agent.update(:test_stats, fn [total: t, passed: p, failed: f] ->
[total: t + 1, passed: p, failed: f + 1]
end)
end
def report_results do
stats = Agent.get(:test_stats, &(&1))
IO.inspect stats
Agent.stop(:test_stats)
end
end
A couple new things here:
-
In
start_run
, passing a:name
as part of theAgent.start
options allows you to reference the agent by that name elsewhere, allowing us to not have to worry about setting a module- or project-level variable. -
Agent.stop
, as it suggests, stops the process. Here, once the results are in, we want to make sure, the next time we run tests, we don’t append our results to the same stats as our previous run.Agent.start
returns{:error, {:already_started, pid}}
if another live process with the same name exists.
Let’s see how this works:
iex> {:ok, agent} = Tester.start_run
iex> Tester.record_pass
iex> Tester.record_pass
iex> Tester.record_fail
iex> Tester.report_results
[total: 3, passed: 2, failed: 1]
iex> Process.alive?(agent)
false
Perfect!
Certainly there are less contrived examples in the world, but hopefully this is enough to give you a basic taste of how state can be managed using agents in Elixir.