惰性求值解释器

通过修改元循环求值器实现正规序语言:参数延迟求值、thunk 的表示与强制、带记忆化的惰性解释器。

核心思想

本节我们要实现一门正规序语言,它和 Scheme 完全相同,唯一的区别是:复合过程对每个参数都是非严格的,原始过程则仍然是严格的。 要改造第 12 课的求值器,让它解释的语言表现出这种行为,并不难。几乎所有需要改动的地方都集中在过程应用上。

(记住,上面这些选择只是选择而已!第 12 课的元循环求值器本身就工作得很好,只是有时候我们希望 Scheme 有不同的行为。本节要讲的就是修改 MCE 的代码,让被解释的 Scheme 变成正规序。)

基本思路是:应用过程时,解释器必须判断哪些参数要求值、哪些要延迟。被延迟的参数不求值,而是被转化成一种叫做 thunk(延迟计算单元)的对象。thunk 必须包含在需要时产生该参数值所需的全部信息,就好像这个值在过程应用的当时就已经求过值一样。因此,thunk 必须包含参数表达式,以及求值该过程应用时所处的环境。

在 thunk 中求值表达式的动作叫做强制(forcing)。一般来说,只有到了需要值的时候,thunk 才会被强制:把它传给一个会用到其值的原始过程时;它是某个条件判断的谓词值时;它是即将作为过程被应用的操作符时。我们有一个设计上的选择:要不要像第 11 课对延迟对象那样,对 thunk 做记忆化(memoization)。有了记忆化,thunk 第一次被强制时会把算出的值存下来,以后再强制就直接返回存好的值,不必重算。我们决定让解释器带上记忆化,因为对很多应用来说这样更高效。不过这里也有些棘手的细节要考虑。

修改求值器

惰性求值器与第 12 课的求值器,主要差别在于 eval 和 apply 中处理过程应用的方式。

eval 中 application? 子句变成:

((application? exp)
 (apply (actual-value (operator exp) env)
        (operands exp)
        env))

这和第 12 课 eval 中的 application? 子句几乎一样。但为了实现惰性求值,我们调用 apply 时传的是操作数表达式本身,而不是把它们求值后得到的参数。由于参数要延迟时构造 thunk 需要环境,我们还得把环境一并传过去。操作符我们仍然要求值,因为 apply 需要拿到实际要应用的过程,才能按类型分发(原始的还是复合的)并应用它。

凡是需要表达式的实际值时,我们都用

(define (actual-value exp env)
  (force-it (eval exp env)))

而不是直接用 eval。这样,如果表达式的值是个 thunk,它就会被强制。

修改 apply

新版的 apply 也和 MCE 中的版本几乎一样。区别在于:eval 传进来的是未求值的操作数表达式——对于原始过程(它们是严格的),先求值所有参数再应用;对于复合过程(它们是非严格的),先把所有参数延迟再应用。

(define (apply procedure arguments env)
  (cond ((primitive-procedure? procedure)
         (apply-primitive-procedure
          procedure
          (list-of-arg-values arguments env)))  ; changed
        ((compound-procedure? procedure)
         (eval-sequence
          (procedure-body procedure)
          (extend-environment
           (procedure-parameters procedure)
           (list-of-delayed-args arguments env) ; changed
           (procedure-environment procedure))))
        (else
         (error
          "Unknown procedure type -- APPLY" procedure))))

处理参数的过程和第 12 课的 list-of-values 很像,区别在于:list-of-delayed-args 是延迟参数而不是求值参数;list-of-arg-values 用 actual-value 而不是 eval:

(define (list-of-arg-values exps env)
  (if (no-operands? exps)
      '()
      (cons (actual-value (first-operand exps) env)
            (list-of-arg-values (rest-operands exps)
                                env))))
(define (list-of-delayed-args exps env)
  (if (no-operands? exps)
      '()
      (cons (delay-it (first-operand exps) env)
            (list-of-delayed-args (rest-operands exps)
                                  env))))

处理 if

另一处必须改动的地方是 if 的处理:必须用 actual-value 而不是 eval 来取得谓词表达式的值,然后再判断它是真还是假:

(define (eval-if exp env)
  (if (true? (actual-value (if-predicate exp) env))
      (eval (if-consequent exp) env)
      (eval (if-alternative exp) env)))

修改 driver-loop

最后,还要修改 driver-loop 过程(即 read-eval-print 循环),把 eval 换成 actual-value。这样,如果延迟的值一路传回到 read-eval-print 循环,打印前会先被强制。我们同时修改提示符,表明这是惰性求值器:

(define input-prompt ";;; L-Eval input:")
(define output-prompt ";;; L-Eval value:")
(define (driver-loop)
  (prompt-for-input input-prompt)
  (let ((input (read)))
    (let ((output
           (actual-value input the-global-environment)))
      (announce-output output-prompt)
      (user-print output)))
  (driver-loop))

测试一下

改完这些,我们就可以启动求值器做测试了。前面”正规序与应用序”一节讨论过的 try 表达式如果能成功求值,就说明解释器在执行惰性求值:

(define the-global-environment (setup-environment))
(driver-loop)
;;; L-Eval input:
(define (try a b)
  (if (= a 0) 1 b))
;;; L-Eval value:
ok
;;; L-Eval input:
(try 0 (/ 1 0))
;;; L-Eval value:
1

表示 thunk

我们的求值器必须安排好在过程应用于参数时创建 thunk,并在稍后强制这些 thunk。thunk 要把表达式和环境打包在一起,这样参数的值以后才能被算出来。强制 thunk 时,我们只需从中取出表达式和环境,在该环境中求值这个表达式。这里用 actual-value 而不是 eval,是为了在表达式的值本身也是 thunk 的情况下继续强制,如此反复,直到得到不是 thunk 的东西:

(define (force-it obj)
  (if (thunk? obj)
      (actual-value (thunk-exp obj) (thunk-env obj))
      obj))

把表达式和环境打包的一个简单办法,是构造一个包含表达式和环境的列表。于是,thunk 这样创建:

(define (delay-it exp env)
  (list 'thunk exp env))

(define (thunk? obj)
  (tagged-list? obj 'thunk))

(define (thunk-exp thunk) (cadr thunk))

(define (thunk-env thunk) (caddr thunk))

实际上,我们的解释器需要的并不完全是这个样子,而是带记忆化的 thunk。thunk 被强制时,我们把它变成一个已求值的 thunk:用值替换掉存储的表达式,并改掉 thunk 的标记,让它以后能被认出来已经求过值了。

(define (evaluated-thunk? obj)
  (tagged-list? obj 'evaluated-thunk))

(define (thunk-value evaluated-thunk)
  (cadr evaluated-thunk))
(define (force-it obj)
  (cond ((thunk? obj)
         (let ((result (actual-value
                        (thunk-exp obj)
                        (thunk-env obj))))
           (set-car! obj 'evaluated-thunk)
           (set-car! (cdr obj) result)  ; replace exp with its value
           (set-cdr! (cdr obj) '())     ; forget unneeded env
           result))
        ((evaluated-thunk? obj)
         (thunk-value obj))
        (else obj)))

请注意:同一个 delay-it 过程,带不带记忆化都能用。