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
- Create the private cell. The enclosing
letallocatesnonce while constructingcounter. - Return behavior, not the cell. The lambda is the only value that can reach that binding after
letfinishes. - 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.