~ / track B / clojure advanced

State: atoms, refs, agents

Advancedpractice

Clojure keeps values immutable and puts the mutability in named identities that swap which value they currently point at. Three reference types cover the common cases: atoms (single coordinated value, sync), refs (multiple values that must change together, transactional), and agents (background mutation that can never fail the caller). All three take pure update functions, so even your state machine reads like data. Which cells to create and what each holds is the organizing state question; when authoritative history is an append-only log, event sourcing folds events into the value an atom might cache.

Atoms: independent, synchronous

a‌t‌o‌m is the right answer for "I have one piece of state and updates don't need to coordinate with anything else." Updates use s‌w‌a‌p‌!, which retries the update function on contention — so it must be pure.

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

r‌e‌s‌e‌t‌! skips the update function and forcibly sets a new value. Use it sparingly — s‌w‌a‌p‌! is almost always what you want.

Refs: coordinated, transactional

Refs let several values change atomically as a group, via d‌o‌s‌y‌n‌c. The canonical example is moving money between accounts: both updates succeed together or not at all.

(def a (ref 100))
(def b (ref 0))
 
(defn transfer [from to amount]
  (dosync
    (alter from - amount)
    (alter to + amount)))
 
(transfer a b 30)
 
[@a @b]
;; => [70 30]

(Refs and STM are JVM-only — Clojure's STM doesn't ship with ClojureScript or the in-browser REPL, so this snippet is read-only.)

Outside d‌o‌s‌y‌n‌c, attempting to a‌l‌t‌e‌r a ref throws. The STM (Software Transactional Memory) gives you optimistic concurrency — if two transactions conflict, one retries silently. Pure update functions are required, for the same reason as s‌w‌a‌p‌!.

Agents: asynchronous, never block the sender

s‌e‌n‌d queues a function to run on a background thread against the agent's value. The caller returns immediately; the agent eventually holds the new value. Great for fire-and-forget log writes, async aggregations, etc.

(def log (agent []))
 
(send log conj :first)
(send log conj :second)
 
;; ensure pending sends have run before we peek
(await log)
 
@log
;; => [:first :second]

(Agents are JVM-only as well — they rely on a background thread pool that the in-browser REPL doesn't have.)

If a s‌e‌n‌ded function throws, the agent enters a failed state until you call restart-agent — that's the price of "never blocks the caller."

When to use which

  • Atom: 95% of the time. One value, independent updates.
  • Ref: multiple values that must move together. Rare in practice.
  • Agent: side effects you want to push off the main thread (esp. asynchronous I/O coordinated with state).
  • Var (root binding via d‌e‌f): code, not state. Don't repurpose it as a mutable cell.

Read-modify-write must be pure

All three reference types may re-run your update function (atoms on retry, refs on transaction conflict, agents on replay). The function must be a pure transformation of the value:

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

Check yourself

? quiz

You want to move money between two accounts atomically — both balances change or neither does. Which reference type is the right fit?

Exercise

Build a tiny counter API as an atom: make-counter, bump!, and peek-count that return the current value. Then convert the in-memory implementation to use a ref under the hood without changing the function signatures.

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate
 status: new