~ / track E / fp foundations

Immutability

Basictheory

In Clojure, the built-in collections are persistent: every "update" returns a brand new value and leaves the original untouched. You don't mutate data; you derive new versions of it. That sounds expensive, but Clojure shares structure between versions, so the cost is small and the guarantees are huge.

Minimal example

c‌o‌n‌j returns a new vector with one more element. The original is unchanged when you look at it again:

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

The same holds for maps. a‌s‌s‌o‌c produces a new map; the original still has the old value:

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

Practical example

Because values do not change behind your back, you can hold onto a snapshot and trust it. Here a "transaction history" keeps every prior state:

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

This is why local reasoning works in Clojure: if you bound xs ten lines ago, nothing your callees do can make xs mean something else now.

Immutability is what makes functions pure

There's a benefit of immutability that often goes unsaid: it's what lets a function be pure, and purity is what lets functions compose. Two related ideas are worth keeping distinct:

  • A pure function returns the same output for the same input. A function that throws is still pure, as long as it throws for the same inputs — purity is about determinism, not about never failing.
  • Referential transparency is stronger: an expression can be replaced by its result without changing the program's meaning. If (f a) evaluates to b, then (g (f a)) and (g b) are interchangeable.

Referential transparency requires immutability — (f a) can only stand in for its result if it produced no side effect and returned a value that won't shift underneath g. And that substitutability is precisely what composition leans on: g builds on f's result without caring how it was produced. See also Purity vs side effects.

Exercise

Define rename-key, which takes a map m, an old key k, and a new key k', and returns a new map with the value moved from k to k'. The original m must not change.

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