Clojure Atom: A Complete Guide
Clojure is known for its powerful concurrency primitives. The most commonly used primitive is the atom. They hold state and let you safely read and modify it from multiple threads. This guide will teach you how they work and give you lots of code examples of how to use them.
My name is Eric Normand. I've used Clojure since 2008. I've taught it online and in person since 2013. I've worked at several companies using Clojure. I use atoms all the time, so I'm happy to share my knowledge and experience with you.
Before we start, I want you to trust that this guide will be worth your time. I guarantee that you will learn to use atoms effectively using this guide. If you read the whole thing and feel like you've still got questions, I'll answer them for you. You can email me. And if that doesn't work, we can schedule a call. That's my guarantee to you. Now let's get started.
What is an atom?
An atom is an object. You can think of it as a box.
The box refers to one value (although that value can be a collection). It's meant to contain only immutable values.
This simple object allows multiple threads to read and write to it safely. Let's look at the interface for atoms.
atom
— Creating an atom
To create an atom, you use the atom
function. It takes one argument, which is
the initial value inside the box.
(atom 0)
Very often, but not always, we will assign the atom to a global variable.
(def counter (atom 0))
I was always taught global mutable state was bad, but that's what we're doing, and it's super common in Clojure. The thing is, we don't make many of them (often just one), and we use them in a very controlled manner. It doesn't feel bad when I use it. It beats the alternative, which is little bits of state hidden in lots of objects.
However, it's also common to use atoms locally, for instance, by assigning them
in a let
:
(let [counter (atom 0)]
...)
deref
— Reading from an atom
To read the value in the atom, you use the function called deref
. deref
will
return the current value of the atom at the time you call it.
(deref counter) ;; => 0
Clojure has a shorthand for deref
: You put a @
in front of the atom.
@counter ;; => 0
swap!
— Modifying the value of an atom
The interface of atoms is very strict, which is what allows them to be shared so easily. The interface may be strict, but it's very easy to use.
We use the function called swap!
to modify the current value of the atom. You
pass it at least two arguments. The first argument is the atom you want to
modify. That makes sense because the first argument is often the thing you're
modifying.
The second argument is a function that takes the current value and returns the new value. This is a mutation function: It's a pure function that returns a modified copy of the first argument.
(swap! counter inc)
inc
is a built-in function that increments a number by one. So this code will
take the current value of counter
, call inc
on it, and set the new value of
counter
to the result. It does this all safely, even if multiple threads are
modifying it at the same time. We'll see why in a bit.
swap!
can take more arguments than just those two. Any arguments you pass it
will be passed, in order, to the mutation function after the current value.
For instance, we can do this:
(swap! counter + 10)
The next value of counter
will be (+ @counter 10)
. These extra arguments
come in handy because a lot of mutation functions need more than just the value
they are modifying.
swap!
will return the new value of the atom.
The mutation function should be pure, that is, it shouldn't have any side
effects. swap!
might run your function multiple times. We'll see why when we
talk about how atoms work.
swap-vals!
— Modifying the value and returning the old value
Sometimes when you modify the value of an atom, you want to know what the old
value was. You can use swap-vals!
for that.
(swap-vals! counter + 10) ;; => [0 10]
We'll see some examples of when this is useful when we see how to use atoms to make a stack and queue.
reset!
— Setting the value of an atom
If you want the atom to take a new value that isn't based on the current value,
you want to use reset!
.
(reset! counter 0)
This will set the value of counter
to 0
, no matter what the current value
is. We use this often to get back to an initial value, though you can also do it
simply to set the value without regard for the current value.
Building a mutable stack
Clojure's vector and list types both implement a stack discipline. You use three functions:
conj
— push a value onto the stackpop
— pop a value off the stack, returning the new stackpeek
— get the top value on the stack
However, vectors and lists are immutable, so we can't use them for mutable state. However, we can use them inside of an atom to use its mutability. Here's how we'll do it.
First, we'll define a global mutable stack using an atom. The stack will start empty.
(def stack (atom []))
Then, we want to have two operations:
push!
pop!
Let's start with push!
:
(defn push! [value]
(swap! stack conj value)
nil)
We use swap!
to modify the current value of the stack. We use conj
to add
the value we pass in. Finally, we return nil
because we want to signal that it
has an effect (it's not pure).
Now let's do pop!
:
(defn pop! []
(let [[old _new] (swap-vals! stack pop)]
(peek old)))
This one is a little more complicated. We use swap-vals!
to get the old and
new values. We don't need the new value, so we ignore it. We call swap-vals!
with the pop
function, which removes the top of the stack and returns the new
stack. Then we call peek on the old value, because we want the top of the
stack from before we popped.
And that's it! Here's a cool tip: You can make a queue by using a
clojure.lang.PersistentQueue
instead of a vector:
(def queue (atom clojure.lang.PersistentQueue/EMPTY))
Then you would define enqueue!
and dequeue!
functions in the same way:
(defn enqueue! [value]
(swap! stack conj value)
nil)
(defn dequeue! []
(let [[old _new] (swap-vals! stack pop)]
(peek old)))
As an exercise, modify these functions to take the stack or queue as arguments, instead of referring to them in the global namespace.
How atoms work
It's time to learn how atoms work.
Atoms give you a safe way to read and modify mutable state across multiple
threads. They do this by using a compare-and-swap operation. This operation is
implemented using the Java class java.util.concurrent.atomic.AtomicReference
.
Compare-and-swap is a way to modify a value only if it hasn't changed since you
last read it. It's implemented in hardware and is thread-safe. It means that we
can read a value, modify it, and write it back atomically.
Compare-and-swap lets you do this atomically:
- Read the current value
- Calculate a new value from the current value
- Set the new value if the current value is still the same
Great! But what happens if it's not the same? That's where the retry loop comes in:
- Read the current value
- Calculate a new value from the current value
- Set the new value if the current value is still the same
- If it's not the same, go back to step 1
This is why the mutation function you pass to swap!
should be pure. It might
get called multiple times. The more threads you have updating the value of the
atom, the more contention you'll have. Contention is when two or more threads
are competing to modify a value. Only one of them will win, and the others will
have to retry, which means they'll have to run the mutation function again. This
is why you should keep the mutation function pure and fast. It will be run in a
loop in the thread until it wins. It sounds expensive, but typically this works
just fine. Plus, reading the value of the atom with deref
is fast and doesn't
contend with other threads.
A theory of time
Atoms let you modify state over time. But there's a deeper theory behind it. It's called the epochal model of time.
The model works like this:
IF the atom starts at a valid state AND
IF the transition functions always produce the correct next states THEN
the observers will always see valid states, no matter when they deref
the atom.
This is the guarantee of the atom. Just make sure your initial state is valid and the transition functions return valid values, and you're good.
You don't have to worry about the order the transitions happen in. You don't have to worry about when you read the value. All you have to worry about, really, are the transition functions, which can even be ensured individually.
Holding many values within one atom
Atoms do not let you coordinate changes across multiple atoms. That is, if you
need to keep two atoms in sync as they change, atoms don't give you a mechanism
to do that. If you need to do that, use a ref
, which has transactional
semantics across multiple refs.
But you can still use atoms for maintaining multiple mutable values. The way you do it is by putting a collection into the atom, typically a map. Each key in the map can hold a different value. Then the function that modifies the atom can modify all of the values in the map.
Let's see an example. Let's say we want to calculate the average of a lot of numbers, each of them calculated in different threads. We need to keep track of the sum and the count. We can do it like this:
(def average (atom {:sum 0 :count 0}))
Then we can swap an update like this:
(defn add-number! [n]
(swap! average (fn [{:keys [sum count]}]
{:sum (+ sum n)
:count (inc count)})))
But I prefer to use the threading macro for this:
(defn add-number! [n]
(swap! average #(-> %
(update :sum + n)
(update :count inc))))
Of course, you can make your map as complex as you need. The update
function
is super useful for working with nested values.
watches — Running a function when the value changes
One cool thing to do with atoms is to watch them. A watch is a function that gets called whenever the value of the atom changes. You can use it to trigger a side effect.
This code will print a message whenever the value of counter
changes:
(def counter (atom 0))
(add-watch counter :log (fn [key atom old new]
(println "Counter changed from" old "to" new)))
add-watch
takes three arguments:
- The atom
- A key (which will get passed to the function to differentiate it from other watches)
- The watch function to call
The watch function needs to take four arguments:
- The key of that watch (in this case,
:log
) - The atom it was called on
- The old value of the atom
- The new value of the atom
The key and atom are passed in in case you want to reuse that function for other atoms and keys.
You can remove a watch later like this:
(remove-watch counter :log)
set-validator!
— Throw out invalid values
We've been talking about valid states in the atom. Atoms come with a way to validate the new value before it's stored in the atom.
You can set the validator for an atom using set-validator!
. The validator is a
function of one argument that returns falsey (or throw an exception) if the new
value is not valid.
Let's say we only want our counter to be non-negative. We can set a validator like this:
(def counter (atom 0))
(set-validator! counter #(not (neg? %)))
Now when we try to set it to something negative, it will throw an exception:
(swap! counter dec)
; Execution error (IllegalStateException) at user/eval8075 (REPL:69).
; Invalid reference state
You can remove the validator like this:
(set-validator! counter nil)
Note that the atom will run the validator when you first set it. If the current value is not valid, it won't let you set the validator:
(def counter (atom 0))
(set-validator! counter pos?) ;; 0 is not positive
; Execution error (IllegalStateException) at user/eval8075 (REPL:69).
; Invalid reference state
compare-and-set!
— Setting the value if it's a certain value
There's one more function in the atom interface that is way less commonly used, but for completeness, let's talk about it. I've never actually used it. But you can if you need it.
compare-and-set!
takes three arguments. The first is the atom you want to
modify. The second argument is the value you expect to be in the atom. And the
new value is the third argument.
compare-and-set!
will only set the value if the current value is identical to
the expected value. If it's not, it will do nothing and return false
. If it
is, it will set the value and return true
.
Note that identical means reference equality. That is, the expected value is
compared to the current value using identical?
. This is important and can
cause problems if you're used to thinking in terms of value equality, which uses
=
.
Conclusion
So that's atoms, the most popular concurrency primitive in Clojure. They let you create a single mutable reference that can be read and modified safely across multiple threads. Their strict interface lets you reason about their behavior easily.
If you're interested in more concurrency primitives, check out my Clojure Concurrency Tutorial, which contains a large catalog of concurrency primitives available in Clojure.
Atoms are used in Reagent for storing state that components react to state changes. You can read about them in my Reagent Tutorial.
If this guide didn't answer your questions, please email me (eric@ericnormand.me) and I'll answer them as best I can. You can also drop into my Office Hours to ask me questions live. I'm happy to help.