6.5 闭包 (Closures)
函数可以如表达式的值,或是其它对象那样被返回。以下是接受一个实参,并依其类型返回特定的结合函数:
(defun combiner (x)
(typecase x
(number #'+)
(list #'append)
(t #'list)))
在这之上,我们可以创建一个通用的结合函数:
(defun combine (&rest args)
(apply (combiner (car args))
args))
它接受任何类型的参数,并以适合它们类型的方式结合。(为了简化这个例子,我们假定所有的实参,都有着一样的类型。)
> (combine 2 3)
5
> (combine '(a b) '(c d))
(A B C D)
2.10 小节提过词法变量(lexical variables)只在被定义的上下文内有效。伴随这个限制而来的是,只要那个上下文还有在使用,它们就保证会是有效的。
如果函数在词法变量的作用域里被定义时,函数仍可引用到那个变量,即便函数被作为一个值返回了,返回至词法变量被创建的上下文之外。下面我们创建了一个把实参加上 3
的函数:
> (setf fn (let ((i 3))
#'(lambda (x) (+ x i))))
#<Interpreted-Function C0A51E>
> (funcall fn 2)
5
当函数引用到外部定义的变量时,这外部定义的变量称为自由变量(free variable)。函数引用到自由的词法变量时,称之为闭包(closure)。 [2] 只要函数还存在,变量就必须一起存在。
闭包结合了函数与环境(environment);无论何时,当一个函数引用到周围词法环境的某个东西时,闭包就被隐式地创建出来了。这悄悄地发生在像是下面这个函数,是一样的概念:
(defun add-to-list (num lst)
(mapcar #'(lambda (x)
(+ x num))
lst))
这函数接受一个数字及列表,并返回一个列表,列表元素是元素与传入数字的和。在 lambda 表达式里的变量 num
是自由的,所以像是这样的情况,我们传递了一个闭包给 mapcar
。
一个更显着的例子会是函数在被调用时,每次都返回不同的闭包。下面这个函数返回一个加法器(adder):
(defun make-adder (n)
#'(lambda (x)
(+ x n)))
它接受一个数字,并返回一个将该数字与其参数相加的闭包(函数)。
> (setf add3 (make-adder 3))
#<Interpreted-Function COEBF6>
> (funcall add3 2)
5
> (setf add27 (make-adder 27))
#<Interpreted-Function C0EE4E>
> (funcall add27 2)
29
我们可以产生共享变量的数个闭包。下面我们定义共享一个计数器的两个函数:
(let ((counter 0))
(defun reset ()
(setf counter 0))
(defun stamp ()
(setf counter (+ counter 1))))
这样的一对函数或许可以用来创建时间戳章(time-stamps)。每次我们调用 stamp
时,我们获得一个比之前高的数字,而调用 reset
我们可以将计数器归零:
> (list (stamp) (stamp) (reset) (stamp))
(1 2 0 1)
你可以使用全局计数器来做到同样的事情,但这样子使用计数器,可以保护计数器被非预期的引用。
Common Lisp 有一个内置的函数 complement
函数,接受一个谓词,并返回谓词的补数(complement)。比如:
> (mapcar (complement #'oddp)
'(1 2 3 4 5 6))
(NIL T NIL T NIL T)
有了闭包以后,很容易就可以写出这样的函数:
(defun our-complement (f)
#'(lambda (&rest args)
(not (apply f args))))
如果你停下来好好想想,会发现这是个非凡的小例子;而这仅是冰山一角。闭包是 Lisp 特有的美妙事物之一。闭包开创了一种在别的语言当中,像是不可思议的程序设计方法。