Skip to content

LISP - 闭包

闭包是 Lisp 及其他函数式语言中最强大的特性之一。它是一个函数,能够捕获并记住其词法环境——即函数创建时作用域内的变量和绑定。这使得函数能够维护状态并在可能已不复存在的上下文中运行。

  • 词法作用域:Lisp 使用词法(或静态)作用域。这意味着变量的作用域由其在源代码中的物理位置决定。一个函数可以访问其包含的代码块中的变量。这就是闭包得以实现的基础。
  • 头等函数:在 Lisp 中,函数是头等公民。它们可以存储在变量中,作为参数传递给其他函数,并从函数中返回结果。闭包是结合了这一特性和词法作用域的直接产物。
  • 环境捕获:当创建 lambda(一个匿名函数)时,它会捕获其引用的词法变量的快照。这个被捕获的环境与函数捆绑在一起,形成了闭包。

演示闭包最常见的方式是使用函数工厂:一个创建并返回其他函数的函数。在这里,我们将创建一个 adder 工厂。

(defun make-adder (n)
"一个函数工厂。返回一个新函数(一个闭包),该函数将其参数加上 N。"
(lambda (x)
(+ x n))) ; 这个 `lambda` 捕获了 `n` 的值
;; --- 创建并使用一些加法器函数 ---
;; 创建两个独立的闭包,每个都有其捕获的环境。
(let ((add-5 (make-adder 5)) ; 这个闭包记住 n=5
(add-10 (make-adder 10))) ; 这个闭包记住 n=10
;; `funcall` 用于调用存储在变量中的函数。
(format t "Calling add-5 with 3: ~a~%" (funcall add-5 3))
(format t "Calling add-10 with 3: ~a~%" (funcall add-10 3))
;; `let` 绑定中的原始 `n` 已经消失,但闭包仍然有效。
(format t "Calling add-5 again with 100: ~a~%" (funcall add-5 100)))

执行代码后,会返回以下结果:

Calling add-5 with 3: 8
Calling add-10 with 3: 13
Calling add-5 again with 100: 105
  • make-adder 函数接受一个参数 n,并立即返回一个 lambda 函数。
  • 返回的 lambda 之所以成为闭包,是因为它引用了 n,而 n 是 make-adder 的词法环境的一部分。
  • 当我们调用 (make-adder 5) 时,会得到一个“硬编码”为加 5 的函数,因为它已经捕获了 n 的那个特定值。
  • 每次调用 make-adder 都会创建一个新的、独立的闭包,拥有其自己的捕获环境。

闭包非常适合创建简单的有状态对象,而无需完整的 defclass。下面是一个创建计数器的函数。

(defun make-counter ()
"创建一个计数器函数,每次调用时都会增加其内部计数。"
(let ((count 0))
(lambda ()
(incf count)))) ; `incf` 递增被捕获的 `count` 变量
(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)))
Counter A: 1
Counter A: 2
Counter B: 1
Counter A: 3

如你所见,counter-a 和 counter-b 是独立的,各自拥有私有的 count 变量,这得益于闭包的强大功能。