[002.2] Processes With State, and Agent

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:

defmoduleStatefulProcessesTestdouseExUnit.Casetest"starting the counter"do{:ok,pid}=Counter.start(0)assertis_pid(pid)endend

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

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.

defmoduleCounterdo# ...defloop(value)doreceivedo# 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)endend# Then we just make the nice function to interact with our process loopdefget_value(pid)doref=make_ref()send(pid,{self(),ref,:get_value})receivedo{:ok,^ref,val}->{:ok,val}endendend

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:

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

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:

defmoduleCounterdodefstart(initial_value)do# Starting an agent just requires giving it a function that returns the# agent's initial value.Agent.start(fn->initial_valueend)enddefget_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)enddefincrement(pid)do# To increment, we send a function that tells the agent what to do with its# state.Agent.update(pid,fn(x)->x+1end)enddefdecrement(pid)do# To decrement, we send a function that tells the agent what to do with its# state.Agent.update(pid,fn(x)->x-1end)endend

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.

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.