~ / track A / clojure basics

Persistent data structures

Intermediate

A persistent data structure preserves every previous version of itself. When you "update" a Clojure vector or map, you get a new value back; the original is still around, unchanged. Naively that would be expensive, but Clojure uses structural sharing: the new value reuses almost all of the old one and only stores what's different.

The result is the best of both worlds: the safety of immutability with operations that are effectively O(log32 n) — close enough to constant for practical purposes.

Minimal example

assoc returns a new map without disturbing the old one. The old binding is still valid, and the two values can coexist:

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

conj on a vector gives the same guarantee — xs stays the same, ys is a fresh value:

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

Structural sharing, by intuition

Under the hood, Clojure vectors are trees of branching factor 32. To "update" an element, the runtime copies the path from the root to the leaf and shares every other branch with the original. For 1,000,000 elements, that's only ~4 levels of copying.

You can observe a side effect of sharing with identical? — when the changed portion is small, deep structural pieces are reused:

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

The "wrapper" is never the same, but the data on either side of the change typically is.

Practical example

Because every prior version still exists, you can keep a full undo history without any explicit copying:

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

reductions returns every intermediate value of the reduce. With mutable structures you'd have to deep-copy at each step; here it's free.

Check yourself

? quiz

Why does Clojure's `assoc` on a million-element vector stay fast even though it returns a new vector?

Exercise

Use assoc-in and update-in to navigate nested data immutably. Given the state below, produce a new state where :players[1].score is 100 and the original state is untouched.

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