~ / track B / clojure advanced
State: atoms, refs, agents
AdvancedpracticeClojure 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
atomclojure.core/atomMutable, synchronous, uncoordinated reference type.view on clojuredocs → is the right answer for "I have one piece of state and updates don't need to coordinate with anything else." Updates use swap!clojure.core/swap!Atomically apply f to the atom's value.view on clojuredocs →, which retries the update function on contention — so it must be pure.
reset!clojure.core/reset!Atomically set the atom's value.view on clojuredocs → skips the update function and forcibly sets a new value. Use it sparingly — swap!clojure.core/swap!Atomically apply f to the atom's value.view on clojuredocs → is almost always what you want.
Refs: coordinated, transactional
Refs let several values change atomically as a group, via dosyncclojure.core/dosyncRun a transaction; ref updates inside are coordinated and retry on conflict.view on clojuredocs →. 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 dosyncclojure.core/dosyncRun a transaction; ref updates inside are coordinated and retry on conflict.view on clojuredocs →, attempting to alterclojure.core/alterIn-transaction update of a ref: (alter r f args*).view on clojuredocs → 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 swap!clojure.core/swap!Atomically apply f to the atom's value.view on clojuredocs →.
Agents: asynchronous, never block the sender
sendclojure.core/sendDispatch a CPU-bound action to an agent.view on clojuredocs → 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 sendclojure.core/sendDispatch a CPU-bound action to an agent.view on clojuredocs →ed 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 defclojure.core/defBind a name to a value in the current namespace.view on clojuredocs →): 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:
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.