赋值其实是一个被大家漠视了的概念,简单的赋值表达式如 x = 3 然后再次赋值 x = 4 这两个x还是同一个x吗?当然这里面涉及到"同一"和“变化”这样的哲学问题,已经超出了讨论的范畴。
但我想说的是,赋值这个操作实际上是为程序引入了时间的概念, 以赋值操作为分界线,事务已经发生了变化。 事实上碰到的各种并发问题的根源就在于赋值,函数式编程就不存在并发问题(当然,函数式编程会有自己的问题, 比如随机数生成过程)。
lisp中的赋值操作是 set!
(define a 3)
(set! a 4)
赋值之后,a的值为4。
有了赋值后,我们可以写出这样一个计数器:
(define (count n)
(let ((value n))
(lambda ()
(set! value (+ 1 value))
value)))
(define a (count 0))
(a)
(a)
这和我们之前求阶乘的程序有了根本性的不同
(define (fact n)
(if (< n 2)
n
(* n (fact (- n 1)))))
(fact 5)
(fact 5)
阶乘程序无论调用多少次,只要输入相同,结果就一样,也就是具有“幂等性”。而这里的计数器有了自己的状态,每次调用的结果都不一样,这也就是赋值带来的变化。
引入了赋值之后,之前表达式求值的代换模型就不再适用了,于是引入了新的模型也就是环境模型。
所谓的环境就是一个个键值对,在其中能找到某个name对应的value。
在环境模型下,过程的定义是在环境中加入了以过程名为名,过程体为值的键值对。也即
(define (name arg) body)
是在环境中加入了这样一个键值对: name: (lambda (arg) body)
而过程的求值,是新生成一个过程形参和实参对应的框架,并链接到过程定义的初始环境上,在这个新的环境上求解过程体的过程。
考虑下面一段程序
(define (sum x y) (+ x y))
(sum 3 4)
(sum 4 5)
首先,有一个全局环境E0,里面包含+、-、*、/、3、4等的定义,这是解释器已经给我们生成好了的环境。
-------------------------
| +: +, 3:3 , 4: 4 |
-------------------------
执行 (sum 3 4)时,环境变为 E2
-------------------------
| +: +, 3:3 , 4: 4 |
-------------------------
/ \
/ \
| sum: (lambda () (+ x y)) | | x = 3, y = 4 | (F1)
在此环境下,求值sum的过程体 (lambda () (+ x y)), 即(+ x y), 结果为7。
执行(sum 4 5)时,环境变为 E3
-------------------------
| +: +, 3:3 , 4: 4 |
--------------------------
/ \
/ \
| sum: (lambda () (+ x y)) | | x = 4, y = 5 | (F2)
在此环境下,求值sum的过程体 (lambda () (+ x y)), 即(+ x y), 结果为9。
这也就是两次函数调用的x和y互不影响的原因,因为求值时生成了两个独立的框架。
赋值操作改变了框架中变量的值,在新的环境中求解表达式即可。