LISP - Closures
Lisp - Closures and Lexical Scope
Section titled “Lisp - Closures and Lexical Scope”A closure is one of the most powerful features in Lisp and other functional languages. It is a function that captures and remembers its lexical environment—the variables and bindings that were in scope when the function was created. This allows a function to maintain state and operate in a context that might otherwise no longer exist.
Core Concepts
Section titled “Core Concepts”- Lexical Scoping: Lisp uses lexical (or static) scope. This means the scope of a variable is determined by its physical placement in the source code. A function can see variables from its containing blocks of code. This is what makes closures possible.
- First-Class Functions: In Lisp, functions are first-class citizens. They can be stored in variables, passed as arguments to other functions, and returned as results from functions. Closures are a direct result of combining this with lexical scoping.
- Environment Capturing: When a
lambda(an anonymous function) is created, it takes a snapshot of the lexical variables it references. This captured environment is bundled with the function, forming the closure.
The Classic Example: A Function Factory
Section titled “The Classic Example: A Function Factory”The most common way to demonstrate closures is with a function factory: a function that creates and returns other functions. Here, we’ll create an adder factory.
main.lisp
Section titled “main.lisp”(defun make-adder (n) "A function factory. Returns a new function (a closure) that adds N to its argument." (lambda (x) (+ x n))) ; This `lambda` captures the value of `n`
;; --- Create and use some adder functions ---
;; Create two distinct closures, each with its own captured environment.(let ((add-5 (make-adder 5)) ; This closure remembers n=5 (add-10 (make-adder 10))) ; This closure remembers n=10
;; `funcall` is used to invoke a function stored in a variable. (format t "Calling add-5 with 3: ~a~%" (funcall add-5 3)) (format t "Calling add-10 with 3: ~a~%" (funcall add-10 3))
;; The original `n` from the `let` binding is gone, but the closures still work. (format t "Calling add-5 again with 100: ~a~%" (funcall add-5 100)))Output
Section titled “Output”When you execute the code, it returns the following result:
Calling add-5 with 3: 8Calling add-10 with 3: 13Calling add-5 again with 100: 105Explanation
Section titled “Explanation”- The
make-adderfunction takes an argumentnand immediately returns alambdafunction. - The returned
lambdabecomes a closure because it referencesn, which is part ofmake-adder’s lexical environment. - When we call
(make-adder 5), we get a function that is hard-coded to add5, because it has captured that specific value ofn. - Each call to
make-addercreates a new, independent closure with its own captured environment.
Practical Use Case: Encapsulating State
Section titled “Practical Use Case: Encapsulating State”Closures are excellent for creating simple, stateful objects without needing a full defclass. Here’s a function that creates a counter.
main.lisp
Section titled “main.lisp”(defun make-counter () "Creates a counter function that increments its internal count on each call." (let ((count 0)) (lambda () (incf count)))) ; `incf` increments the captured `count` variable
(let ((counter-a (make-counter)) (counter-b (make-counter)))
(format t "Counter A: ~a~%" (funcall counter-a)) (format t "Counter A: ~a~%" (funcall counter-a))
(format t "Counter B: ~a~%" (funcall counter-b)) (format t "Counter A: ~a~%" (funcall counter-a)))Output
Section titled “Output”Counter A: 1Counter A: 2Counter B: 1Counter A: 3As you can see, counter-a and counter-b are independent objects, each with its own private count variable, thanks to the power of closures.