作用域
作用域,是自由变量的查找规则。
如果变量具有词法作用域,
语言实现会相继到更外层的词法范围内查找绑定值。
如果变量具有动态作用域,
语言实现会回溯到更早的嵌套调用中查找绑定值。
词法作用域
(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年
我们想到,
闭包和对象,只是用不同的方法实现了封装。
而这种封装思想,才是更值得关注的。
编程范型之争愈演愈烈,
函数式和面向对象之间似乎水火不容,
我们可不要在讨论手段的同时,偏废了思想。
结语
封装,具有深刻的内涵,
它有几层含义,表达了很多与编程范型无关的思想,
“封装的内涵”和大家一起详细探讨了这些内容。