灯下 登录
计算机科学 / SICP / subsection 中英对照

4.2.2 An Interpreter with Lazy Evaluation

In this section we will implement a normal-order language that is the same as

Scheme except that compound procedures are non-strict in each argument.

Primitive procedures will still be strict. It is not difficult to modify the

evaluator of 4.1.1 so that the language it interprets behaves

this way. Almost all the required changes center around procedure application.

本节我们将实现一种正则序语言,它与 Scheme 相同,只是复合过程对每个实参都是非严格的。基本过程仍然是严格的。要将 4.1.1 节的求值器修改为使其所解释的语言具有这种行为,并不困难。几乎所有必要的改动都集中在过程应用的处理上。

The basic idea is that, when applying a procedure, the interpreter must

determine which arguments are to be evaluated and which are to be delayed. The

delayed arguments are not evaluated; instead, they are transformed into objects

called

thunks. The thunk must

contain the information required to produce the value of the argument when it

is needed, as if it had been evaluated at the time of the application. Thus,

the thunk must contain the argument expression and the environment in which the

procedure application is being evaluated.

基本思想是:在应用过程时,解释器必须判断哪些实参需要被求值、哪些需要被延迟。被延迟的实参不予求值,而是被转化为称为 thunk 的对象。thunk 必须包含在需要该实参值时能够产生这个值所需的全部信息,就好像它在应用时已被求值一样。因此,thunk 必须同时包含实参表达式以及过程应用求值时所处的环境。

The process of evaluating the expression in a thunk is called

对 thunk 中的表达式进行求值的计算过程称为

forcing.

In general, a thunk will be forced only when its value is needed: when it is

passed to a primitive procedure that will use the value of the thunk; when it

is the value of a predicate of a conditional; and when it is the value of an

operator that is about to be applied as a procedure. One design choice we have

available is whether or not to

memoize thunks, as we did with delayed

objects in 3.5.1. With memoization, the first time a thunk is

forced, it stores the value that is computed. Subsequent forcings simply

return the stored value without repeating the computation. We’ll make our

interpreter memoize, because this is more efficient for many applications.

There are tricky considerations here, however.

Subheading: Modifying the evaluator

强迫 (forcing)。一般而言,一个 thunk 只在其值真正被需要时才会被强迫:当它被传递给一个需要用到该值的基本过程时;当它是某个条件判断的谓词值时;以及当它是即将作为过程被应用的运算符的值时。我们面临的一个设计选择,是是否对 thunk 进行记忆化 (memoize),正如我们在 3.5.1 节对延迟对象所做的那样。采用记忆化后,一个 thunk 在第一次被强迫时会将计算所得的值存储起来,此后再次强迫时只需直接返回已存储的值,而无需重复计算。我们将使解释器支持记忆化,因为这对许多应用而言效率更高。不过,这里也存在一些需要仔细考量的棘手问题。

小标题:修改求值器

The main difference between the lazy evaluator and the one in 4.1

is in the handling of procedure applications in eval and apply.

The application? clause of eval becomes

惰性求值器与 4.1 节中的求值器之间的主要区别,在于 `eval` 和 `apply` 中对过程应用的处理。`eval` 中处理 `application?` 的子句变为

This is almost the same as the application? clause of eval in

4.1.1. For lazy evaluation, however, we call apply with

the operand expressions, rather than the arguments produced by evaluating them.

Since we will need the environment to construct thunks if the arguments are to

be delayed, we must pass this as well. We still evaluate the operator, because

apply needs the actual procedure to be applied in order to dispatch on

its type (primitive versus compound) and apply it.

Whenever we need the actual value of an expression, we use

这与 4.1.1 节中 `eval` 的 `application?` 子句几乎相同。但对于惰性求值,我们在调用 `apply` 时传入的是运算对象表达式本身,而不是对它们求值后得到的实参。由于在需要延迟实参时,我们需要用到环境来构造 thunk,所以也必须将环境一并传入。我们仍然对运算符进行求值,因为 `apply` 需要实际的过程才能根据其类型(基本过程还是复合过程)进行分派并应用它。每当需要某个表达式的实际值时,我们使用

instead of just eval, so that if the expression’s value is a thunk, it

will be forced.

而不仅仅是 `eval`,这样一来,如果某个表达式的值是一个 thunk,它就会被强迫。

Our new version of apply is also almost the same as the version in

4.1.1. The difference is that eval has passed in

unevaluated operand expressions: For primitive procedures (which are strict),

we evaluate all the arguments before applying the primitive; for compound

procedures (which are non-strict) we delay all the arguments before applying

the procedure.

我们新版的 `apply` 与 4.1.1 节中的版本也几乎相同。区别在于,`eval` 传入的是尚未被求值的运算对象表达式:对于基本过程(严格的),我们在应用基本过程之前先对所有实参求值;对于复合过程(非严格的),则在应用过程之前将所有实参延迟。

The procedures that process the arguments are just like list-of-values

from 4.1.1, except that list-of-delayed-args delays the

arguments instead of evaluating them, and list-of-arg-values uses

actual-value instead of eval:

处理实参的那些过程与 4.1.1 节中的 `list-of-values` 几乎相同,只是 `list-of-delayed-args` 对实参进行延迟而不是求值,`list-of-arg-values` 使用 `actual-value` 而不是 `eval`:

The other place we must change the evaluator is in the handling of if,

where we must use actual-value instead of eval to get the value

of the predicate expression before testing whether it is true or false:

另一处需要修改求值器的地方,是对 `if` 的处理——在判断谓词表达式的真假之前,必须使用 `actual-value` 而非 `eval` 来获取谓词表达式的值:

Finally, we must change the driver-loop procedure (4.1.4)

to use actual-value instead of eval, so that if a delayed value

is propagated back to the read-eval-print loop, it will be forced before being

printed. We also change the prompts to indicate that this is the lazy

evaluator:

最后,我们必须修改 `driver-loop` 过程(4.1.4 节),使其使用 `actual-value` 而非 `eval`,这样一来,若某个延迟值被传播回读入-求值-打印循环,它会在打印之前被强迫。我们还修改了提示符,以表明这是惰性求值器:

With these changes made, we can start the evaluator and test it. The

successful evaluation of the try expression discussed in

4.2.1 indicates that the interpreter is performing lazy evaluation:

完成以上修改后,我们可以启动求值器并对其进行测试。在 4.2.1 节中讨论的 `try` 表达式能够被成功求值,说明解释器正在执行惰性求值:

Subheading: Representing thunks

小标题:表示 thunk

Our evaluator must arrange to create thunks when procedures are applied to

arguments and to force these thunks later. A thunk must package an expression

together with the environment, so that the argument can be produced later. To

force the thunk, we simply extract the expression and environment from the

thunk and evaluate the expression in the environment. We use

actual-value rather than eval so that in case the value of the

expression is itself a thunk, we will force that, and so on, until we reach

something that is not a thunk:

我们的求值器必须安排好:在过程被应用于实参时创建 thunk,并在稍后强迫这些 thunk。thunk 必须将一个表达式与一个环境打包在一起,以便日后能够产生该实参的值。要强迫一个 thunk,只需从中提取出表达式和环境,然后在该环境中对表达式求值即可。我们使用 `actual-value` 而非 `eval`,这样一来,如果表达式的值本身也是一个 thunk,我们就会继续强迫它,如此循环,直到得到一个非 thunk 的值:

One easy way to package an expression with an environment is to make a list

containing the expression and the environment. Thus, we create a thunk as

follows:

将表达式与环境打包在一起的一种简便方法,是构造一个包含该表达式和该环境的表。因此,我们按如下方式创建 thunk:

Actually, what we want for our interpreter is not quite this, but rather thunks

that have been memoized. When a thunk is forced, we will turn it into an

evaluated thunk by replacing the stored expression with its value and changing

the thunk tag so that it can be recognized as already

evaluated.

实际上,我们的解释器需要的并不完全是这个,而是经过记忆化的 thunk。当一个 thunk 被强迫时,我们将把它转化为一个已求值的 thunk (evaluated thunk),方法是用其值替换所存储的表达式,并修改 thunk 的标签,使其能够被识别为已经求值过的。

Notice that the same delay-it procedure works both with and without

memoization.

注意,同一个 `delay-it` 过程在有记忆化和无记忆化的情况下都能正常工作。

Racket #lang sicp
((application? exp)
 (apply (actual-value (operator exp) env)
 (operands exp)
 env))
Racket #lang sicp
(define (actual-value exp env)
 (force-it (eval exp env)))
Racket #lang sicp
(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))))
Racket #lang sicp
(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))))
Racket #lang sicp
(define (eval-if exp env)
 (if (true? (actual-value (if-predicate exp)
 env))
 (eval (if-consequent exp) env)
 (eval (if-alternative exp) env)))
Racket #lang sicp
(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))
Racket #lang sicp
(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
Racket #lang sicp
(define (force-it obj)
 (if (thunk? obj)
 (actual-value (thunk-exp obj)
 (thunk-env obj))
 obj))
Racket #lang sicp
(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))
Racket #lang sicp
(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)
 ;; replace exp with its value:
 (set-car! (cdr obj) result)
 ;; forget unneeded env:
 (set-cdr! (cdr obj) '())
 result))
 ((evaluated-thunk? obj)
 (thunk-value obj))
 (else obj)))