6.5 闭包 (Closures)

函数可以如表达式的值,或是其它对象那样被返回。以下是接受一个实参,并依其类型返回特定的结合函数:

  1. (defun combiner (x)
  2. (typecase x
  3. (number #'+)
  4. (list #'append)
  5. (t #'list)))

在这之上,我们可以创建一个通用的结合函数:

  1. (defun combine (&rest args)
  2. (apply (combiner (car args))
  3. args))

它接受任何类型的参数,并以适合它们类型的方式结合。(为了简化这个例子,我们假定所有的实参,都有着一样的类型。)

  1. > (combine 2 3)
  2. 5
  3. > (combine '(a b) '(c d))
  4. (A B C D)

2.10 小节提过词法变量(lexical variables)只在被定义的上下文内有效。伴随这个限制而来的是,只要那个上下文还有在使用,它们就保证会是有效的。

如果函数在词法变量的作用域里被定义时,函数仍可引用到那个变量,即便函数被作为一个值返回了,返回至词法变量被创建的上下文之外。下面我们创建了一个把实参加上 3 的函数:

  1. > (setf fn (let ((i 3))
  2. #'(lambda (x) (+ x i))))
  3. #<Interpreted-Function C0A51E>
  4. > (funcall fn 2)
  5. 5

当函数引用到外部定义的变量时,这外部定义的变量称为自由变量(free variable)。函数引用到自由的词法变量时,称之为闭包(closure)。 [2] 只要函数还存在,变量就必须一起存在。

闭包结合了函数与环境(environment);无论何时,当一个函数引用到周围词法环境的某个东西时,闭包就被隐式地创建出来了。这悄悄地发生在像是下面这个函数,是一样的概念:

  1. (defun add-to-list (num lst)
  2. (mapcar #'(lambda (x)
  3. (+ x num))
  4. lst))

这函数接受一个数字及列表,并返回一个列表,列表元素是元素与传入数字的和。在 lambda 表达式里的变量 num 是自由的,所以像是这样的情况,我们传递了一个闭包给 mapcar

一个更显着的例子会是函数在被调用时,每次都返回不同的闭包。下面这个函数返回一个加法器(adder):

  1. (defun make-adder (n)
  2. #'(lambda (x)
  3. (+ x n)))

它接受一个数字,并返回一个将该数字与其参数相加的闭包(函数)。

  1. > (setf add3 (make-adder 3))
  2. #<Interpreted-Function COEBF6>
  3. > (funcall add3 2)
  4. 5
  5. > (setf add27 (make-adder 27))
  6. #<Interpreted-Function C0EE4E>
  7. > (funcall add27 2)
  8. 29

我们可以产生共享变量的数个闭包。下面我们定义共享一个计数器的两个函数:

  1. (let ((counter 0))
  2. (defun reset ()
  3. (setf counter 0))
  4. (defun stamp ()
  5. (setf counter (+ counter 1))))

这样的一对函数或许可以用来创建时间戳章(time-stamps)。每次我们调用 stamp 时,我们获得一个比之前高的数字,而调用 reset 我们可以将计数器归零:

  1. > (list (stamp) (stamp) (reset) (stamp))
  2. (1 2 0 1)

你可以使用全局计数器来做到同样的事情,但这样子使用计数器,可以保护计数器被非预期的引用。

Common Lisp 有一个内置的函数 complement 函数,接受一个谓词,并返回谓词的补数(complement)。比如:

  1. > (mapcar (complement #'oddp)
  2. '(1 2 3 4 5 6))
  3. (NIL T NIL T NIL T)

有了闭包以后,很容易就可以写出这样的函数:

  1. (defun our-complement (f)
  2. #'(lambda (&rest args)
  3. (not (apply f args))))

如果你停下来好好想想,会发现这是个非凡的小例子;而这仅是冰山一角。闭包是 Lisp 特有的美妙事物之一。闭包开创了一种在别的语言当中,像是不可思议的程序设计方法。