Identity and Change
Programs model change: people marry, engines wear out, forests regrow. Yet we still refer to these evolving things by a single name. That notion—an identity spanning different values over time—is what distinguishes mutable references (identities) from the immutable values they point to.
So far, the bindings and arguments we’ve used are immutable references: once a symbol is bound to a value, that value can’t change. Functions like inc return new values; they never alter their inputs. Clojure extends this immutability to collections, let bindings, and function arguments. Critically, when a function is constructed, it “closes over” the values of any non-argument symbols in its body, retaining them for later invocation.
That closure property is the foundation for more interesting identities. By deferring evaluation, we can introduce concurrency—expressions running outside their normal sequential order—and then build up to parallel and synchronized state.
Delaying Work
When you write a function, its body isn’t evaluated immediately. You can use this to defer arbitrary expressions, which is handy for expensive computations you might not need right away. The standard macro for this is delay:
user=> (do (prn "Adding") (+ 1 2))
"Adding"
3
user=> (def later (fn [] (prn "Adding") (+ 1 2)))
#'user/later
user=> (later)
"Adding"
3
Rather than returning a function, delay creates a Delay object—an identity that refers to an unevaluated expression. (It macroexpands into an anonymous function, so it closes over lexical scope just like any other function.) You pull the value out with deref, or the shorthand @:
user=> (source delay)
(defmacro delay
"Takes a body of expressions and yields a Delay object that will
invoke the body only the first time it is forced (with force or deref/@), and
will cache the result and return it on all subsequent force
calls. See also - realized?"
{:added "1.0"}
[& body]
(list 'new 'clojure.lang.Delay (list* `^{:once true} fn* [] body)))
Unlike calling a plain function, a Delay evaluates its expression once; after the first deref, it remembers the result and returns it for every subsequent dereference. Since the computation is postponed until you actually ask for it, this is a form of lazy evaluation—useful for IOUs and avoiding unnecessary work. To get lazy evaluation so it doesn’t accidentally compute. The shortcut @ derefs a Delay.
Futures: Parallel Deferral
Delays defer work until you ask, but what if you want to start a computation immediately and still not block on the result? That’s exactly what a future does: it’s a delay evaluated in parallel on another thread. A future returns right away, giving you an identity that will eventually hold the expression’s value:
user=> (def x (future (prn "hi") (+ 1 2)))
"hi"
#'user/x
user=> (deref x)
3
Because futures run on separate threads, your program’s expressions can be interleaved—even run on separate cores simultaneously. Results are only computed once, and you can deref the future any number of times to get the same value. Futures are the most general parallel tool in Clojure’s toolbox, suitable for CPU-heavy work, waiting on multiple network requests, or scheduling background housekeeping.
Promises: Filling the Box Later
Where delays and futures control when evaluation happens, promises let you defer the supply of a value. A promise starts out empty—pending—and you fill it once. Any attempt to deref an empty promise will block until a value is delivered:
user=> (deref box)
If nothing ever delivers a value, that thread waits forever. The deliver function fills the promise, and like a real-world promise, there’s no going back—you can’t deliver twice or change the value afterward.
Because a promise guarantees that readers wait for the writer, it’s a synchronization primitive. You can orchestrate concurrent code with promises, letting one thread signal another when data is ready.
Vars: Globals and Dynamic Scope
So far, all these identities eventually resolve to a single, fixed value. But sometimes you need a name whose value changes over time—a global. That’s where vars come in. When you def something, you’re creating a var, a mutable reference that can be rebound. When a var is evaluated, it transparently yields its current value:
user=> (defn decouple [glider]
#_=> (prn "bolts released"))
#'user/decouple
user=> (defn launch [glider]
#_=> (decouple glider)
#_=> (prn glider "away!"))
#'user/launch
user=> (launch "albatross")
"bolts released"
"albatross" "away!"
nil
user=> (defn decouple [glider]
#_=> (prn "tether released"))
#'user/decouple
user=> (launch "albatross")
"tether released"
"albatross" "away!"
Using vars for mutable state has a serious caveat: because vars are global, every part of the program sees the same value. This makes them a poor fit for application state; changing a var can have ripple effects anywhere. Clojure reserves vars for naming functions and tracking program-wide environment settings (e.g., which database to use, where prn should write).
That said, vars have one more trick: dynamic scope. Mark a var with ^:dynamic and give it a conventional asterisk-wrapped name:
user=> (def ^:dynamic *board* :maple)
#'user/*board*
Unlike lexical scope—which is fixed by the literal text of the function—dynamic scope propagates through function calls. The binding macro overrides the var’s value for the duration of the binding expression and for every function called from within it, no matter how deeply nested. Crucially, this thread-local safety holds even in concurrent programs: only the thread that entered the binding sees the override; other threads are unaffected.
This makes dynamic vars useful for passing contextual state—like a “current” user, locale, or logging level—without threading it through every function argument. But for general mutable program state, vars are a trap: global mutable references lead to unpredictable, hard-to-reason-about code. For that, we need something more disciplined—the subject of what comes next.
Managing Change
Vars let you read, set, and dynamically bind names, but they don't handle evolution well. Building a set of integers incrementally with def follows an imperative idiom familiar from C, Ruby, or JavaScript:
user=> (def xs #{})
#'user/xs
user=> (dotimes [i 10] (def xs (conj xs i)))
user=> xs
#{0 1 2 3 4 5 6 7 8 9}
This pattern—read, modify, redefine—is straightforward until threads get involved. Run the same accumulation across ten parallel threads and the read-modify-update steps stop being consecutive; they become concurrent, and updates get lost:
- Thread 2 reads
#{0 1} - Thread 3 reads
#{0 1} - Thread 2 writes
#{0 1 2} - Thread 3 writes
#{0 1 3}
That interleaving drops the number 2 entirely. What's needed is an identity that supports safe transformation from one state to another. That's what atoms provide.
user=> (def xs (atom #{}))
#'user/xs
user=> xs
#<Atom@30bb8cc9: #{}>
Atoms are created with an initial value—here, the empty set #{}. They're not transparent the way vars are; evaluating an atom doesn't yield its underlying value directly, though printing shows it. You must use deref or @ to pull the current value out. And where you'd def a var, an atom uses reset!. The bang suffix marks mutating functions: reset! changes the atom's value wholesale.
user=> (reset! xs :foo)
:foo
user=> xs
#<Atom@30bb8cc9: :foo>
Unlike vars, atoms can be safely updated with swap!, which applies a pure function to the current value and stores the result. Clojure guarantees these updates are linearizable: all swap! operations complete in what appears to be a single consecutive order, no effect happens before swap! is called, and the effect is visible to everyone once it returns. Additional arguments to swap! are passed along to the function, so (swap! x + 5 6) computes (+ x 5 6).
user=> (def xs (atom #{}))
#'user/xs
user=> (dotimes [i 10] (future (swap! xs conj i)))
nil
user=> @xs
#{0 1 2 3 4 5 6 7 8 9}
The update function passed to swap! must be pure—it can't mutate any state—because Clojure may call it more than once when resolving thread conflicts. Immutable datatypes and pure functions are what make this kind of safe, linearizable mutation possible.
Coordinated Transactions
Atoms are individually linearizable, but updates across multiple atoms aren't ordered relative to one another. For multi-identity changes, you need serializability: a global order across the group. Clojure's identity type for this is the Ref.
user=> (def x (ref 0))
#'user/x
user=> x
#<Ref@1835d850: 0>
Refs dereference like other identities:
user=> @x
0
But instead of swap!, refs update in groups inside a dosync transaction. ref-set assigns a new value to a ref—and unlike reset!, you can change multiple refs in one transaction:
user=> (def x (ref 0))
user=> (def y (ref 0))
user=> (dosync
(ref-set x 1)
(ref-set y 2))
2
user=> [@x @y]
[1 2]
The closer analog to swap! is alter:
user=> (def x (ref 1))
user=> (def y (ref 2))
user=> (dosync
(alter x + 2)
(alter y inc))
3
user=> [@x @y]
[3 3]
All alter operations inside a dosync happen atomically; their effects never interleave with other transactions. When the order of updates doesn't matter, commute offers a faster alternative by relaxing ordering constraints. Since x + 2 + 3 equals x + 3 + 2, commutative operations can complete in any order with the same result—a weaker but quicker safety property.
For reading one ref and using it to update another, use ensure rather than deref. ensure performs a strongly consistent read, one that takes its logical place in the transaction's order:
user=> (dosync
(alter x + (ensure y)))
Refs make complex transactional logic safer, but that safety isn't free: updating refs is typically about an order of magnitude slower than atoms. Choose refs only when multiple pieces of state need coordinated updates across overlapping parts of a system. If there's no overlap, separate atoms work fine; if all operations touch the same identities, one atom holding a map may be cleaner.
Choosing the Right Tool
The constructs explored here each solve a different part of the state problem. Vars are mutable, transparent names—handy but not thread-safe for read-modify-write cycles. Delays, futures, and promises handle deferred or parallel computation, with deref blocking until the value is available. That blocking behavior is itself a synchronization mechanism for concurrent threads.
Atoms and refs differ on the read side too: they can be read immediately at any time, but writes belong inside swap! or a dosync transaction respectively. Atoms suit single-identity updates; refs handle coordinated, transactional updates without sacrificing consistency. For updates that apply to a whole system in lockstep, a single atom holding an immutable map often beats a collection of refs.



