What a Closure Captures
A closure is a function bundled together with the lexical environment in which it was created, so it can still access variables from that surrounding scope even after the enclosing form has finished executing. In LISP, (lambda (x) (+ x y)) captures whatever binding of y was visible at the point the lambda was written; if that y came from an enclosing let, the lambda 'closes over' it and keeps a live reference to it, not just a snapshot of its value at creation time.
Cricket analogy: A closure is like a bowler who permanently remembers the exact field placement set during a specific over, so even brought back later in the match he still 'sees' that same original fielding arrangement rather than whatever is set up now.
Building Closures with lambda and let
A classic example is a counter generator: (defun make-counter () (let ((n 0)) (lambda () (incf n)))) returns a fresh lambda each time it's called, and each returned lambda closes over its own private n, initialized to 0 in that particular call's let. Calling one counter's lambda repeatedly increments only its own n; a second call to make-counter creates an entirely separate n and an entirely separate closure, so the two counters never interfere with each other.
Cricket analogy: This is like two separate tally counters clicked independently by two different scorers tracking two different bowlers' wicket counts in the same match, each counter's tally is private and never bleeds into the other's.
(defun make-counter ()
(let ((n 0))
(lambda () (incf n))))
(defparameter *counter-a* (make-counter))
(defparameter *counter-b* (make-counter))
(funcall *counter-a*) ; => 1
(funcall *counter-a*) ; => 2
(funcall *counter-b*) ; => 1 -- independent state, unaffected by *counter-a*Closures for Data Hiding and Encapsulation
Closures give LISP a lightweight alternative to formal object-oriented encapsulation: the private state (n above) is only reachable through the closure's own function calls, since nothing outside make-counter's body has a reference to that particular n binding. This pattern shows up throughout idiomatic LISP: memoization functions that close over a private cache hash table, generator-style iterators that close over their current position, and callback handlers that close over configuration captured at setup time.
Cricket analogy: This is like a team's private strategy notebook that only the specific analyst who wrote it can access, since no other staff member holds a reference to that exact notebook, a lightweight form of information hiding without a formal filing system.
Tip: memoization is a classic use of closures — write a function that closes over a private hash-table cache, checks the cache first, and only computes (and stores) a result on a cache miss. The cache is invisible and unreachable to any code outside the closure.
Shared vs Separate Environments
It's important to distinguish closures that share one environment from closures that each get their own: if you build a list of lambdas inside a single let binding a loop variable that gets mutated in place, all those lambdas can end up closing over the same binding and therefore all see its final value, a classic pitfall in languages with imperative loop variables. LISP's dotimes and dolist in Common Lisp actually rebind the loop variable fresh on each iteration in most implementations, but the underlying rule to remember is: a closure captures the binding (the variable slot), not a frozen copy of the value, so anything that mutates a shared binding after the fact is visible to every closure that captured it.
Cricket analogy: This is like three commentators all referring to 'the current batsman' from a single shared scoreboard variable that keeps updating, by the time they each read it aloud, all three end up describing whoever is batting last, not who was in at the moment they each spoke.
Watch out for closures that unintentionally share a mutable binding: if several lambdas are created inside the same loop iteration or the same let and all reference a variable that gets mutated afterward, they'll all observe the final mutated value, not the value at the time each lambda was created. Rebind a fresh variable per iteration (or per closure) if each one needs its own private snapshot.
- A closure bundles a function with the lexical environment (variable bindings) visible where it was defined.
- Each call to a closure-returning function like make-counter creates a fresh, independent set of bindings, so separate closures don't interfere with each other.
- Closures capture the binding itself, not a frozen snapshot of its value, mutations to a shared binding are visible to every closure that captured it.
- Closures provide a lightweight alternative to formal object encapsulation: private state is reachable only through the closure's own functions.
- Common idioms built on closures include memoization caches, stateful iterators, and callbacks that close over setup-time configuration.
- A frequent pitfall is multiple closures accidentally sharing one mutable binding instead of each getting its own private copy.
Practice what you learned
1. What does a closure capture?
2. In the make-counter example, why do two separate calls to make-counter produce independent counters?
3. What is a classic pitfall involving closures and shared bindings?
4. How do closures provide a form of encapsulation?
5. Which of these is a typical real-world use of closures in LISP?
Was this page helpful?
You May Also Like
Dynamic vs Lexical Scope
How lexical scope resolves variables by source-text structure while dynamic scope resolves them by the call stack, and how Common Lisp uses both via special variables.
Code as Data: Homoiconicity
Why LISP source code and LISP data share the same list representation, and how this property enables macros, metaprogramming, and eval.
Macros in LISP
How LISP macros transform unevaluated code at compile time to create new syntax, and how to write them safely with defmacro, gensym, and hygiene practices.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics