Closures and Shared Mutable State

A closure captures lexical cells, not frozen value copies, so related procedures can coordinate through one private binding.

See it run

LISPEX
(define counter (let ((n 0)) (lambda () (set! n (+ n 1)) n)))
(list (counter) (counter))

Observed result

OUTPUT
(1 2)

Walk through the pattern

  1. Create the private cell. The enclosing let allocates n once while constructing counter.
  2. Return behavior, not the cell. The lambda is the only value that can reach that binding after let finishes.
  3. Observe shared updates. Both calls invoke the same closure, so the second call sees the first call’s set!.

How to reason about it

  • Create the state in an enclosing let, return procedures that read or mutate it, and keep the cell unreachable except through those procedures.
  • Shadowed bindings allocate different cells; set! updates the nearest resolved binding.
  • Top-level redefinition updates the original global cell seen by earlier closures.

Check yourself

Construct counter-a and counter-b by evaluating the enclosing let twice. Does calling counter-a change counter-b?

Answer

No. Each evaluation allocates a different lexical cell. Calls share state only when their closures capture the same cell.

A common mistake

Shared mutation is deterministic here, but it still deserves narrow ownership and explicit tests.

Keep going

Bindings owns the exact cell and shadowing rules. Use the decision-rule guide if state can instead be explicit input.

Bindings and scope · Decision rules