Jade Dungeon

ch03 模块化、对象和状态 part02

求值环境模型

在引入赋值以后,变量就不能被视为某个值的名字,而是代表在环境中的一个位置。

  • 环境是栈帧的序列,代表了表达式的上下文。
  • 栈帧是约束变量值与名字的表格。
  • 栈帧还有一个指针,指向它的外部环境。
  • 如果栈帧是全局的,则没有外部环境。

一个变量相对于某个特定环境的值,就是包含这个变量的第一个栈帧的约束值。 如果整个栈帧序列(也就是环境)中没有这个变量的约束,则称: 「该变量在该特定环境中无约束」。

例:

  • I、II、III三个栈帧。
  • A、B、C、D是环境指针。
  • 变量mxyz在不同栈帧里「约束」的值不同。
  • 下一级栈帧的约束会屏蔽上一级栈帧里的约束。

简单的环境结构

查找变量的值就从当前栈帧开始,按指针一级一级向上查找。

求值规则

应用「求值代换模型」对组合表达式的求值方法非常简单:

  1. 对每个子表达式求值。
  2. 把操作应用到各个子表达式。

但是「求值环境模型」里就复杂得多。

求值环境模型中定义过程:

每个过程都要描述为一个对偶,分别为:

  1. 代码:过程被转换为lambda表达式。
  2. 环境:产生过程时的环境。

例如定义过程square

(define (square x)
  (* x x))

其实是对lambda表达式定义别名的语法糖:

(define square
  (lambda (x) (* x x)))

可以理解为在全局环境中,把lambda表达式关联到square上:

全局环境中求值

求值环境模型中调用过程

  • 调用过程就会建立一个新的环境,指向了定义实参与形参的约束的栈帧。
  • 栈帧的外围环境就是过程对象所指的环境。

例如调用(square 5),在调用时会建立一个把参数x绑定为5新的栈帧。环境E1 指向这个新栈帧:

调用过程

建立结束:defineset!

  • define在当前栈帧是建立一个「符号」与「值」之间的约束。
  • (set! <variable> <value>)修改约束的值,需要:
    1. 找到包含变量约束的第一个栈帧。
    2. 修改这个栈帧中变量的约束。
    3. 如果变量在环境中没有约束,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)

  1. (f 5)生成:
    • 过程(sum-of-squares (+ a 1) (* a 2))
    • 环境E1,其中约束a = 5
  2. (sum-of-squares (+ 5 1) (* 5 2))生成:
    • 过程(+ (square x) (square y))
    • 环境E2中约束x = 6y = 10
  3. (square 6)生成:
    • 过程(* x x)
    • 环境E3中约束x = 6
  4. (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)))))

ex0309 ex0309 ex0309

迭代版本,为了节约空间,将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表示为 ffact-iter表示为iproduct表示为pcounter表示为cmax-count 表示为m):

ex0309

ex0309

将栈帧看作局部状态的展台

结合之前介绍的提款机程序,演示求值环境过程。

例:定义构造器

(define (make-withdraw balance)
  (lambda (amount)
    (if (>= balance amount)
        (begin (set! balance (- balance amount))
               balance)
        "Insufficient funds")))

这里定义过程本身也是一个lambda表达式。关联到全局环境:

调用过程

例:执行构造器

以余额为参数,构造提款机:

(define W1 (make-withdraw 100))

调用时:

  1. 程序为lambda表达式本身。
  2. 创建了新环境E1,约束了balance = 100,。
  3. 在全局环境中约束过程执行结果为W1

调用过程

例:取款操作

(W1 50)                            ;; 50

构造新栈帧,约束了amount = 50。注意新环境要以E1为外围环境, 因为对象W1所在的环境是E1

与环境对应的过程为:

(if (>= balance amount)
    (begin (set! balance (- balance amount))
           balance)
    "Insufficient funds")))

调用过程

从上图中可以看到,代码中对应的变量balanceamount可以按环境一级一级向上找到 。

在执行到用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创建以下过程对象:

习题3.10

(define w1 (make-withdraw 100))

习题3.10

因为make-withdraw的函数体内又是一个函数调用,所以以上的求值又引发:

首先是创建又一个过程对象:

习题3.10

而这个新的过程对象会即刻被调用,继而产生又一个新环境:

习题3.10

(lambda (balance) ...)的体内是另一个 lambda 表达式(lambda (amount) ...), 因此我们要为它创建又一个过程对象:

习题3.10

(make-withdraw 100)的求值过程到此就暂告一段落了,这时,可以将符号w1和所得 的过程对象建立约束(bundle)了:

习题3.10

使用之前求值得到的w1,执行表达式(w1 50),会创建以下环境:

习题3.10

环境E3在执行过程体中的表达式之后消失,E2balance被设置为50, 以下是求值完毕之后的环境图:

习题3.10

最后,定义另一个make-withdraw实例w2,它的功能性和w1类似, 但是却保存着自己的一簇状态变量和过程对象(最明显的就是它们各自的balance变量) :

习题3.10

内部定义

在局部过程中:

  • 局部过程的名称不会与父过程外的过程名冲突,因为局部过程名是定义在外部过程 运行时临时创建的环境中。
  • 局部过程只把父过程的形参视为自由变量,因为局部过程的环境是父过程环境的子环境。

可以环境模型解释内部定义结构。比如求平方根的程序:

(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))

在定义过程中:

  1. sqrt过程体被作为lambda表达式与全局环境作为一个序对。
  2. 全局环境中约束了sqrt与lambda表达式的关联。

调用(sqrt 2)时:

  1. 以全局环境为外围生成环境E1,绑定xgood-enough?improvesqrt-iter
  2. 执行(sqrt-iter 1.0)E1下生成新环境E2,约束参数guess = 1
  3. 执行good-enough?E1下生成新环境E3,约束参数guess = 1

虽然有多个对guess的约束,但它们是不同过程的参数(sqrt-itergood-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))

两个账户是如何区分的?那些部分是acc1acc2共享的?

先转为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))

过程生成的环境模型如下:

习题3.11

(define acc (make-account 50))

习题3.11

最后,将以上求值得到的值和符号acc关联(前面的求值会返回dispatch,于是acc 就直接指向E1环境中的dispatch过程对象):

习题3.11

((acc 'deposit) 40)

习题3.11

(acc 'deposit)将返回过程deposit,这个deposit又作为新的过程操作符,被参数 40应用,并且E2在求值之后消失:

习题3.11

表达式在E3环境中求值,沿着外围环境指针查找并修改balance的值,求值完毕之后, E3消失:

习题3.11

以上就是求值之后得到的环境,注意balance的值已经被修改为90了。

然后,进行第二次求值((acc 'withdraw) 60),得出以下环境:

习题3.11

(withdraw 60)

习题3.11

以下是求值完毕之后的环境:

习题3.11

注意balance已经被修改为30了。

最后,如果我们进行定义(define acc2 (make-account 100)),那么会产生另一个 make-account过程对象,这个过程并不和acc共享任何的过程或者状态变量, 具体的环境定义如下:

习题3.11