ch03 模块化、对象和状态 part02
求值环境模型
在引入赋值以后,变量就不能被视为某个值的名字,而是代表在环境中的一个位置。
- 环境是栈帧的序列,代表了表达式的上下文。
- 栈帧是约束变量值与名字的表格。
- 栈帧还有一个指针,指向它的外部环境。
- 如果栈帧是全局的,则没有外部环境。
一个变量相对于某个特定环境的值,就是包含这个变量的第一个栈帧的约束值。 如果整个栈帧序列(也就是环境)中没有这个变量的约束,则称: 「该变量在该特定环境中无约束」。
例:
- I、II、III三个栈帧。
- A、B、C、D是环境指针。
-
变量
m
、x
、y
、z
在不同栈帧里「约束」的值不同。 - 下一级栈帧的约束会屏蔽上一级栈帧里的约束。
查找变量的值就从当前栈帧开始,按指针一级一级向上查找。
求值规则
应用「求值代换模型」对组合表达式的求值方法非常简单:
- 对每个子表达式求值。
- 把操作应用到各个子表达式。
但是「求值环境模型」里就复杂得多。
求值环境模型中定义过程:
每个过程都要描述为一个对偶,分别为:
- 代码:过程被转换为lambda表达式。
- 环境:产生过程时的环境。
例如定义过程square
:
(define (square x) (* x x))
其实是对lambda表达式定义别名的语法糖:
(define square (lambda (x) (* x x)))
可以理解为在全局环境中,把lambda表达式关联到square
上:
求值环境模型中调用过程
- 调用过程就会建立一个新的环境,指向了定义实参与形参的约束的栈帧。
- 栈帧的外围环境就是过程对象所指的环境。
例如调用(square 5)
,在调用时会建立一个把参数x
绑定为5
新的栈帧。环境E1
指向这个新栈帧:
建立结束:define
与set!
-
define
在当前栈帧是建立一个「符号」与「值」之间的约束。 -
(set! <variable> <value>)
修改约束的值,需要:- 找到包含变量约束的第一个栈帧。
- 修改这个栈帧中变量的约束。
-
如果变量在环境中没有约束,
set!
会报错。
简单过程应用
定义三个过程:
(define (square x) (* x x)) (define (sum-of-squares x y) (+ (square x) (square y))) (define (f a) (sum-of-squares (+ a 1) (* a 2)))
调用过程(f 5)
:
-
(f 5)
生成:-
过程
(sum-of-squares (+ a 1) (* a 2))
-
环境
E1
,其中约束a = 5
。
-
过程
-
(sum-of-squares (+ 5 1) (* 5 2))
生成:-
过程
(+ (square x) (square y))
-
环境
E2
中约束x = 6
、y = 10
。
-
过程
-
(square 6)
生成:-
过程
(* x x)
-
环境
E3
中约束x = 6
。
-
过程
-
(square 10)
生成:-
过程
(* x x)
-
环境
E3
中约束x = 10
。
-
过程
注意:
-
每个
square
调用都会生成一个包含x
的新环境。可以看到在不同的栈帧中, 同名变量不是同一个。 -
由
square
创建的每个栈帧都指向全局环境(即square
过程对象所指的环境)。
练习 3.9
用求值环境模型描述阶乘过程(factorial 6)
的递归和迭代版本。
递归版本:
(define (factorial n) (if (= n 1) 1 (* n (factorial (- n 1)))))
迭代版本,为了节约空间,将factorial
表示为f
:
(define (factorial n) (fact-iter 1 1 n)) (define (fact-iter product counter max-count) (if (> counter max-count) product (fact-iter (* counter product) (+ counter 1) max-count)))
以下是对(factorial 6)
求值时所创建的环境结构(为了节约空间,factorial
表示为
f
,fact-iter
表示为i
,product
表示为p
,counter
表示为c
,max-count
表示为m
):
将栈帧看作局部状态的展台
结合之前介绍的提款机程序,演示求值环境过程。
例:定义构造器
(define (make-withdraw balance) (lambda (amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) "Insufficient funds")))
这里定义过程本身也是一个lambda表达式。关联到全局环境:
例:执行构造器
以余额为参数,构造提款机:
(define W1 (make-withdraw 100))
调用时:
- 程序为lambda表达式本身。
-
创建了新环境E1,约束了
balance = 100
,。 -
在全局环境中约束过程执行结果为
W1
例:取款操作
(W1 50) ;; 50
构造新栈帧,约束了amount = 50
。注意新环境要以E1
为外围环境,
因为对象W1
所在的环境是E1
。
与环境对应的过程为:
(if (>= balance amount) (begin (set! balance (- balance amount)) balance) "Insufficient funds")))
从上图中可以看到,代码中对应的变量balance
和amount
可以按环境一级一级向上找到
。
在执行到用set!
语句修改balance
值的操作时,环境E1
中约束的balance
值被修改,
但栈帧E1
依然是W1
所代表的过程对象的环境。相对的,包含amount
值的那个栈帧
在(W1 50)
调用完成后已经没有用了。如下图所示:
下一次调用W1
这个过程会有新的栈帧来约束新的实参。虽然会有新的栈帧,
但这个新栈帧的外围环境还是E1
。
例:创建新的提款机
(define W2 (make-withdraw 100))
创建新的提款机W2
会建立新的环境E2
来约束balance = 100
,与前一个环境E1
与不干扰:
练习:3.10
之前介绍说,let
操作:
(let ((<var> <exp>)) <body>)
其实也是lambda的语法糖,相当于:
((lambda (<var>) <body>) <exp>)
可以修改提款机程序,用let
代替参数来作为局部变量,如:
(define (make-withdraw initial-amount) (let ((balance initial-amount)) (lambda (amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) "Insufficient funds")))) (define W1 (make-withdraw 100)) (W1 50) (define W2 (make-withdraw 100))
并用环境模型描述执行过程。
过程可以转为lambda
的实现:
(define make-withdraw (lambda (initial-amount) ((lambda (balance) (lambda (amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) "Insufficient funds"))) initial-amount)))
这个 lambda 版本的make-withdraw
创建以下过程对象:
(define w1 (make-withdraw 100))
:
因为make-withdraw
的函数体内又是一个函数调用,所以以上的求值又引发:
首先是创建又一个过程对象:
而这个新的过程对象会即刻被调用,继而产生又一个新环境:
(lambda (balance) ...)
的体内是另一个 lambda 表达式(lambda (amount) ...)
,
因此我们要为它创建又一个过程对象:
对(make-withdraw 100)
的求值过程到此就暂告一段落了,这时,可以将符号w1
和所得
的过程对象建立约束(bundle)了:
使用之前求值得到的w1
,执行表达式(w1 50)
,会创建以下环境:
环境E3
在执行过程体中的表达式之后消失,E2
的balance
被设置为50
,
以下是求值完毕之后的环境图:
最后,定义另一个make-withdraw
实例w2
,它的功能性和w1
类似,
但是却保存着自己的一簇状态变量和过程对象(最明显的就是它们各自的balance
变量)
:
内部定义
在局部过程中:
- 局部过程的名称不会与父过程外的过程名冲突,因为局部过程名是定义在外部过程 运行时临时创建的环境中。
- 局部过程只把父过程的形参视为自由变量,因为局部过程的环境是父过程环境的子环境。
可以环境模型解释内部定义结构。比如求平方根的程序:
(define (sqrt x) (define (good-enough? guess) (< (abs (- (square guess) x)) 0.001)) (define (improve guess) (average guess (/ x guess))) (define (sqrt-iter guess) (if (good-enough? guess) guess (sqrt-iter (improve guess)))) (sqrt-iter 1.0))
在定义过程中:
-
sqrt
过程体被作为lambda表达式与全局环境作为一个序对。 -
全局环境中约束了
sqrt
与lambda表达式的关联。
调用(sqrt 2)
时:
-
以全局环境为外围生成环境
E1
,绑定x
、good-enough?
、improve
、sqrt-iter
。 -
执行
(sqrt-iter 1.0)
,E1
下生成新环境E2
,约束参数guess = 1
。 -
执行
good-enough?
,E1
下生成新环境E3
,约束参数guess = 1
。
虽然有多个对guess
的约束,但它们是不同过程的参数(sqrt-iter
和good-enough?
)
而且也不在同一个栈帧里。
练习 3.11
用环境计算模型解释以下账户操作的执行过程:
(define (make-account balance) (define (withdraw amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) "Insufficient funds")) (define (deposit amount) (set! balance (+ balance amount)) balance) (define (dispatch m) (cond ((eq? m 'withdraw) withdraw) ((eq? m 'deposit) deposit) (else (error "Unknown request -- MAKE-ACCOUNT" m)))) dispatch)
(define acc (make-account 50)) ((acc 'deposit) 40) ;; 90 ((acc 'withdraw) 60) ;; 30
如果建立另一个账户:
(define acc2 (make-account 100))
两个账户是如何区分的?那些部分是acc1
和acc2
共享的?
先转为lambda表达式:
;;; 11-make-account-using-lambda.scm (define make-account (lambda (balance) (define withdraw (lambda (amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) "Insufficient funds"))) (define deposit (lambda (amount) (set! balance (+ balance amount)))) (define dispatch (lambda (m) (cond ((eq? m 'withdraw) withdraw) ((eq? m 'deposit) deposit) (else (error "Unknown request -- MAKE-ACCOUNT" m))))) dispatch))
过程生成的环境模型如下:
(define acc (make-account 50))
:
最后,将以上求值得到的值和符号acc
关联(前面的求值会返回dispatch
,于是acc
就直接指向E1
环境中的dispatch
过程对象):
((acc 'deposit) 40)
:
(acc 'deposit)
将返回过程deposit
,这个deposit
又作为新的过程操作符,被参数
40
应用,并且E2
在求值之后消失:
表达式在E3
环境中求值,沿着外围环境指针查找并修改balance
的值,求值完毕之后,
E3
消失:
以上就是求值之后得到的环境,注意balance
的值已经被修改为90
了。
然后,进行第二次求值((acc 'withdraw) 60)
,得出以下环境:
(withdraw 60)
:
以下是求值完毕之后的环境:
注意balance
已经被修改为30
了。
最后,如果我们进行定义(define acc2 (make-account 100))
,那么会产生另一个
make-account
过程对象,这个过程并不和acc
共享任何的过程或者状态变量,
具体的环境定义如下: