~ / track B / clojure advanced
State: atoms, refs, agents
AdvancedClojure 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.
Atoms: independent, synchronous
atom is the right answer for "I have one piece of state and updates don't
need to coordinate with anything else." Updates use swap!, which retries the
update function on contention — so it must be pure.
reset! skips the update function and forcibly sets a new value. Use it
sparingly — swap! is almost always what you want.
Refs: coordinated, transactional
Refs let several values change atomically as a group, via dosync. The
canonical example is moving money between accounts: both updates succeed
together or not at all.
Outside dosync, attempting to alter 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!.
Agents: asynchronous, never block the sender
send 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.
If a sended 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
def): 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.