闭包就是对象

作用域

作用域,是自由变量的查找规则。


如果变量具有词法作用域,

语言实现会相继到更外层的词法范围内查找绑定值。


如果变量具有动态作用域,

语言实现会回溯到更早的嵌套调用中查找绑定值。


词法作用域

(define test-lexical-binding

    (let [(x 1)]

        (lambda (y) (+ x y))))


(test-lexical-binding 2)

=> 3


其中,let表达式返回了一个函数,作为test-lexical-binding的值。

根据函数调用规则,我们知道,

(test-lexical-binding 2)

-> ((lambda (y) (+ x y)) 2)

-> (+ x 2)

变量x是自由变量。


如果x具有词法作用域,

则x的值,就是x所在函数,在定义时,外层作用域的值。

(lambda (y) (+ x y))的外层是let表达式,

(let [(x 1)]

    ...)


因此,x => 1,(+ x 2) => 3


动态作用域

(define parameter-object

    (make-parameter 1))


(define (test-dynamic-binding)

    (parameter-object))


    

(parameterize [(parameter-object 2)]

    (test-dynamic-binding))

=> 2


(test-dynamic-binding)

=> 1


其中,(make-parameter 1)返回一个包含值1的参数对象#<parameter object>

参数对象是一个无参函数,调用后会得到它当前状态的包含值。

(parameter-object)的值取决于参数对象所处的动态作用域环境。


我们可以使用parameterize来更改参数对象的包含值,

并且parameterize表达式内部会在新的动态作用域环境中求值。


(parameterize [(parameter-object 2)]

    (test-dynamic-binding))

-> (test-dynamic-binding)

-> (parameter-object)


(parameter-object)要查找调用过程中最近的绑定值,

为了查找调用过程中最近的绑定,我们沿着刚才的推导向上找,

找到了parameterize对它的更改,值为2。

所以,

(parameterize [(parameter-object 2)]

    (test-dynamic-binding))

=> 2


而最后的直接调用(test-dynamic-binding) ,

调用过程中最近的绑定是对参数对象parameter-object的定义,

(define parameter-object

    (make-parameter 1))

所以,(test-dynamic-binding) => 1


词法闭包

如果变量具有动态作用域,我们就要一直记着函数的调用过程

这在复杂的程序中,是很困难又容易出错的事情。

因此,Scheme中的变量,默认具有词法作用域。


词法作用域,保存了变量定义时的环境

起到了封闭和隔离的作用。


例如:

(define-values (get-value set-value!)

    (let [(field 0)]

        (values (lambda () field)

            (lambda (new-value) (set! field new-value)))))


(get-value)

=> 0


(set-value! 1)


(get-value)

=> 1


其中,values表达式用来同时返回多值,而define-values用来定义多值。


get-value和set-value!函数分别用来读取和修改词法作用域中的变量field。

field对于get-value和set-value!来说是共享的

而其它任何函数都无法修改和访问它。


正因为有这样的封闭性,我们将函数连同定义时的环境一起,称为闭包


对象

熟悉面向对象编程的人们,可能会清晰的认识到。

对象同样也是封闭和隔离了它包含的字段。

因此,在这种封装意义上来说,闭包就是对象


那么面向对象语言中的其它概念,是否也有相似的对应关系呢?

有的。


例如:

(define create-object

    (lambda (init)

        (let [(field init)]

            (values (lambda () field)

                (lambda (new-value) (set! field new-value))))))


(define-values (get-value set-value!)

    (create-object 1))


(get-value)

=> 1


(set-value! 2)


(get-value)

=> 2


我们定义了个函数create-object,它可以用来生成对象。

相当于一个对象工厂,面向对象编程中与之对应的概念就是“类”


例如:

(define-values create-object

    (let [(static 1)]

        (lambda (x)

            (let [(field x)]

                (values (lambda () (+ static field))

                    (lambda (new-value) (set! field new-value)))))


最外层的let表达式返回了一个函数create-object,

我们来使用create-object创建两个对象。


(define-values (get-value1 set-value1!)

    (create-object 2))


(get-value1)

=> 3


(set-value1 3)


(get-value1)

=> 4


(define-values (get-value2 set-value2!)

    (create-object 3))


(get-value2)

=> 4


(set-value1 4)


(get-value1)

=> 5


结果,最外层let表达式中的变量static,可以同时被两个对象访问。

在面向对象编程中,与之对应的概念就是“类的静态变量”


思想比手段更重要

我们看到[let返回lambda],就是一个“对象”,

[lambda返回[let返回lambda]],就是一个“类”,

[let返回[lambda返回[let返回lambda]]],就为类增加了“静态变量”。


这是多么简洁而有力的结论呀。

出自——《Let Over Lambda》2008年


我们想到,

闭包和对象,只是用不同的方法实现了封装

而这种封装思想,才是更值得关注的。


编程范型之争愈演愈烈,

函数式和面向对象之间似乎水火不容,

我们可不要在讨论手段的同时,偏废了思想。


结语


封装,具有深刻的内涵,

它有几层含义,表达了很多与编程范型无关的思想,

封装的内涵”和大家一起详细探讨了这些内容。