在“
[原]重新开始学习Scheme(1) ”中提到了Scheme,或者说是函数式编程的一些基本概念,这些概念使得Scheme区别于其他的编程语言,也使得函数式编程FP区别于其他的编程范式。之前用了四篇博文详细讲述了递归以及尾递归,并给出了许多实际的范例。尤其是“
[原]Scheme线性递归、线性迭代示例以及循环不变式”,详细讲述了如何设计并实现尾递归。下面,来看看第三个概念:闭包
“在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这些被引用的自由变量将和这个函数一同存在,即使已经离开了创造它们的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。”这是维基百科给出的说明。
Paul Graham在On Lisp一书中对于闭包的定义则为:函数与一系列变量绑定的组合即是闭包。其实这里也隐含了一个计算环境的问题,那就是函数定义的计算环境。
Closure的示例如下:
- (define closure-demo
- (let ((y 5))
- (lambda (x)
- (set! y (+ y x))
- y)
- )
- )
这里使用了set!,因此其封装了一个状态,即自由变量y:
- > (closure-demo 5)
- 10
- > (closure-demo 5)
- 15
- > (closure-demo 5)
- 20
“闭包可以用来在一个函数与一组“私有”变量之间建立关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所建立所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。”
看到这里,我们马上就能想到一个概念:面向对象。根据对“对象”的经典定义——对象有状态、行为以及标识;对象的行为和结构在通用的类中定义——可以得到,如果使用闭包,很轻松便可以定义一个类。另外,由于向对象发消息需要一个实例,一些参数,并得到发送消息之后的结果,因此,使用一个dispatcher便可以向对象发送消息。例如:
- (define (make-point-2D x y)
- (define (get-x) x)
- (define (get-y) y)
- (define (set-x! new-x) (set! x new-x))
- (define (set-y! new-y) (set! y new-y))
- (lambda (selector . args) ; a dispatcher
- (case selector
- ((get-x) (apply get-x args))
- ((get-y) (apply get-y args))
- ((set-x! (apply set-x! args))
- ((set-y! (apply set-x! args))
- (else (error "don't understand " selector)))))
在这里,make-point-2D是一个函数,它接受两个参数,并返回一个闭包——由lambda定义的一个匿名函数。这个闭包中,引用的自由变量有:get-x,get-y,set-x!, set-y!。这些变量其实是函数,因为函数是一等公民,因此可以用变量将其进行传递。这就是一个基本的2D point类。该类的使用如下:- > (define p1 (make-point-2D 10 20))
- > (p1 'get-x)
- 10
- > (p1 'get-y)
- 20
- > (p1 'set-x! 5)
- > (p1 'set- 10)
- > (list (p1 'get-x) (p1 'get-y))
注意,这些自由变量自己本身又是函数,有自己的计算环境,而它们所访问的变量也是自由变量,因此它们也是闭包,它们的计算环境由lambda定义的匿名函数提供——lambda定义的dispatcher是个大闭包,get-*和set-*都是这个闭包里的闭包。
利用闭包,还可以实现继承,如:
- (define (make-point-3D x y z) ; that is, point-3D _inherits_ from point-2D
- (let ((parent (make-point-2D x y)))
- (define (get-z) z)
- (define (set-z! new-z) (set! z new-z))
- (lambda (selector . args)
- (case selector
- ((get-z) (apply get-z args))
- ((set- (apply set- args)) ; delegate everything else to the parent
- (else (apply parent (cons selector args)))))))
这里面除了make-point-2D的闭包之外,还增加了get-z、set-z!以及lambda定义的匿名函数三个闭包。
在此基础上,利用宏对Scheme进行扩展,便可以得到一个通用的面向对象编程范式框架。当然,不能像在这里一样使用quote的串来确定应该调用哪个函数。
有个帖子讨论为什么Scheme不提供内置OO系统。我同意Abhijat的观点。OO主要目的是封装、模块化、大规模编程、状态,区分了数据和操作。Scheme不区分数据和函数,强调无状态,且函数为一等公民,因此并不需要OO。但实践中很难做到无状态,因此为了保持最小原则,OO由各实现自行添加。