# 4.2.2惰性评估口译员

> 译者： [https://sicp.comp.nus.edu.sg/chapters/83](https://sicp.comp.nus.edu.sg/chapters/83)

在本节中，我们将实现一种与JavaScript相同的普通顺序语言，只是复合函数在每个参数中都是非严格的。 原始函数仍将严格。 修改 [4.1.1](74) 部分的求值器并不难，以使它解释的语言以这种方式运行。 几乎所有必需的更改都围绕功能应用程序进行。

基本思想是，在应用函数时，解释器必须确定要评估哪些自变量以及哪些应延迟。 延迟的参数不被评估； 相反，它们被转换为称为 _thunk_ 的对象。 [[1]](83#footnote-1)

转换程序必须包含在需要时产生自变量值所需的信息，就像在应用程序时已对其进行了评估一样。 因此，实体必须包含参数表达式和正在评估函数应用程序的环境。

评估thunk中的表达式的过程称为_强制_。 [[2]](83#footnote-2)

通常，只有在需要thunk的值时才会强制使用thunk：将其传递给将使用thunk的值的基本函数时； 当它是条件谓词的值时； 以及将要用作函数的运算符的值时。 我们可以使用的一种设计选择是_是否记住_暴动，就像我们在 [3.5.1](67) 部分中对延迟对象所做的那样。 使用备忘录时，第一次强制使用thunk时，它将存储计算出的值。 随后的强制仅返回存储的值，而无需重复计算。 我们将使解释器记忆化，因为这对于许多应用程序来说效率更高。 但是，这里有一些棘手的注意事项。 [[3]](83#footnote-3)

## 修改评估者

惰性评估器与 [4.1](73) 一节中的主要区别在于在`evaluate`和`apply`中处理功能应用程序。

`evaluate`的`is_application`子句变为

```js
is_application(exp)      
? apply(actual_value(operator(exp), env),
        operands(exp),
        env)
```

这几乎与 [4.1.1](74) 部分中`evaluate`的`is_application`子句相同。 但是，对于延迟求值，我们将`apply`与操作数表达式一起调用，而不是通过评估它们产生的参数。 因为如果要延迟参数，我们将需要环境来构造thunk，所以我们也必须通过它。 我们仍然评估运算符，因为`apply`需要实际应用的功能，以便分派其类型（原始与复合）并应用它。

每当我们需要表达式的实际值时，我们都会使用

```js
function actual_value(exp, env) {
    return force_it(evaluate(exp, env));
}
```

instead of just `evaluate`, so that if the expression's value is a thunk, it will be forced.

我们的新版本`apply`也几乎与 [4.1.1](74) 部分中的版本相同。 区别在于`evaluate`传递了未求值的操作数表达式：对于原始函数（严格的），我们在应用原始函数之前先评估所有参数； 对于复合函数（非严格函数），我们在应用该函数之前会延迟所有参数。

```js
function apply(fun, args) {
   if (is_primitive_function(fun)) {
       return apply_primitive_function(
                  fun,  // following line changed
                  list_of_arg_values(args, env));
   } else if (is_compound_function(fun)) {
      const result =
         evaluate(function_body(fun),
                  extend_environment(
                      function_parameters(fun),
                        // following line changed
                      list_of_delayed_args(args,
                                           env),
                      function_environment(fun)));
      if (is_return_value(result)) {
         return return_value_content(result);
      } else {
          return undefined;
      }
   } else {
      Error("Unknown function type in apply",
            fun);
   }
}
```

处理自变量的函数类似于 [4.1.1](74) 部分中的`list_of_values`，除了`list_of_delayed_args`延迟参数而不是对参数求值，并且`list_of_arg_values`使用`actual_value` ]代替`evaluate`：

```js
function list_of_arg_values(exps, env) {
    return no_operands(exps)	
        ? null
        : pair(actual_value(first_operand(exps), 
                            env),
               list_of_arg_values(rest_operands(exps),
                                  env));
}
function list_of_delayed_args(exps, env) {				  
    return no_operands(exps)
        ? null
        : pair(delay_it(first_operand(exps), env),
               list_of_delayed_args(
                   rest_operands(exps), env));
}
```

我们必须更改评估器的另一个地方是`if`的处理，在测试它是对还是错之前，我们必须使用`actual-value`而不是`eval`来获取谓词表达式的值：

```js
function eval_conditional_expression(exp, env) {	
    return is_true(actual_value(cond_expr_pred(exp),
                                env))
           ? evaluate(cond_expr_cons(exp), env)
           : evaluate(cond_expr_alt(exp), env);
}
```

最后，我们必须更改`eval_toplevel`功能（第 [4.1.4](77) 节）以使用`actual_value`而不是`evaluate`，这样如果延迟值传播回了 评估者，它将在被打印之前被强制执行。

```js
function eval_toplevel(stmt) {
   const value = actual_value(stmt, the_global_environment);
   if (is_return_value(value)) {
       error("return not allowed " +
             "outside of function definitions");
   } else {
       return value;
   }
}
```

完成这些更改后，我们可以启动评估器并进行测试。 [4.2.1](82) 节中讨论的`try`表达式的成功评估表明解释器正在执行惰性评估：

```js
eval_toplevel(parse(
   "function try(a, b) {        " +
   "    return a === 0 ? 1 : b; " +
   "}                           " +
   "try(0, head(null));         "   ));
```

## 代表暴徒

我们的评估人员必须安排在将函数应用于参数时创建重击，并在以后强制这些重击。 杂项必须将表达式与环境打包在一起，以便以后可以产生该参数。 要强制重击，我们只需从重击中提取表达式和环境，然后评估环境中的表达式即可。 我们使用`actual_value`而不是`evaluate`，以便在表达式的值本身是一个thunk的情况下，将强制执行该操作，依此类推，直到达到非thunk的值为止：

```js
function force_it(obj) {	
     return is_thunk(obj)
            ? actual_value(thunk_exp(obj), thunk_env(obj))
            : obj;
}
```

将表达式与环境打包在一起的一种简单方法是创建一个包含表达式和环境的列表。 因此，我们创建一个thunk，如下所示：

```js
function delay_it(exp, env) {	
    return list("thunk", exp, env);
}
function is_thunk(obj) {    
    return is_tagged_list(obj, "thunk");
}
function thunk_exp(thunk) {
    return head(tail(thunk));
}
function thunk_env(thunk) {
    return head(tail(tail(thunk)));
}
```

实际上，我们想要的口译员不只是这个，而是已经记住的重击。 当强制重击时，通过将存储的表达式替换为其值并更改`thunk`标签，可以将其转变为已评估的重击，以便将其识别为已评估。 [[4]](83#footnote-4)

```js
function is_evaluated_thunk(obj) {
    return is_tagged_list(obj, "evaluated_thunk");
}
function thunk_value(evaluated_thunk) {
    return head(tail(evaluated_thunk));
}
function force_it(obj) {
    if (is_thunk(obj)) {
        const result = actual_value(
                           thunk_exp(obj),
                           thunk_env(obj));
        set_head(obj, "evaluated_thunk");
        // replace exp with its value
        set_head(tail(obj), result);
        // forget unneeded env
        set_tail(tail(obj), null);
        return result;	
    } else if(is_evaluated_thunk(obj)) {
        return thunk_value(obj);
    } else {
        return obj;
    }
}
```

请注意，相同的`delay_it`函数在带有和不带有备注的情况下均有效。

<exercise>Suppose we type in the following definitions to the lazy evaluator:

```js
let count = 0;
function id(x) {
    count = count + 1;
    return x;
}
```

Give the missing values in the following sequence of interactions, and explain your answers.[[5]](83#footnote-5)

```js
read_eval_print_loop("");
// enter:    <count and id as defined above>
// response: ?
// enter:    const w = id(id(10));
// response: ?
// enter:    count
// response: ?
// enter:    w
// response: ?
// enter:    count
// response: ?
```

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_83_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>The function `evaluate` uses `actual_value` rather than `evaluate` to evaluate the operator before passing it to `apply`, in order to force the value of the operator. Give an example that demonstrates the need for this forcing.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_83_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Exhibit a program that you would expect to run much more slowly without memoization than with memoization. Also, consider the following interaction, where the `id` function is defined as in exercise <ref name="ex:delay-side-effects">[4.18](83#ex_4.18)</ref> and `count` starts at 0:

```js
read_eval_print_loop("");
// enter:    <count and id as defined above>
// response: ?
// enter:    function square(x) { return x * x; }
// response: ?
// enter:    square(id(10));
// response: ?
// enter:    count
// response: ?
```

Give the responses both when the evaluator memoizes and when it does not.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_83_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Cy D. Fect, a reformed C programmer, is worried that some side effects may never take place, because the lazy evaluator doesn't force the expressions in a sequence. Since the value of an expression in a sequence other than the last one is not used (the expression is there only for its effect, such as assigning to a variable or printing), there can be no subsequent use of this value (e.g., as an argument to a primitive function) that will cause it to be forced. Cy thus thinks that when evaluating sequences, we must force all expressions in the sequence except the final one. He proposes to modify `eval-sequence` from section <ref name="sec:core-of-evaluator">[4.1.1](74)</ref> to use `actual_value` rather than `evaluate`:

```js
function eval_sequence(exps, env) {	
    if (is_last_exp(exps)) {
        evaluate(first_exp(exps), env);
    } else {
        actual_value(first_exp(exps), env);
        eval_sequence(rest_exps(exps), env);
    }
}
```

1.  Ben Bitdiddle认为Cy是错误的。 他向Cy展示了练习 [2.23](30#ex_2.23) 中描述的`for_each`功能，该示例提供了具有副作用的序列的重要示例：

    ```js
    function for_each(fun, items) {
        if (is_null(items)){
            return undefined;
        } else {
            fun(head(items));
            for_each(fun, tail(items));
        }
    }
    ```

    [ 他声称案文中的评估者（带有原文`eval_sequence`）正确地处理了此问题：

    ```js
    read_eval_print_loop("");
    // enter:    &lt;for_each as defined above&gt;
    // response: ?
    // enter:    for_each(x =&gt; display(x), list(57, 321, 88));
    // response: 57 
    //           321
    //           88
    // response: done
    ```

    解释Ben为什么对`for_each`的行为是正确的。
2.  Cy同意Ben对`for_each`示例是正确的，但是他说，当他提议将其更改为`eval_sequence`时，他并不是在考虑那种程序。 他在惰性评估器中定义了以下两个函数：

    ```js
    function p1(x) {
        x = pair(x, list(2));
    }
    function p2(x) {
        function p(e) {
            e;
            return x;
        }	
        x = pair(x, list(2));
        return p(x);
    }
    ```

    带有原始`eval_sequence`的`p(1)`和`p2(1)`的值是什么？ Cy建议将`eval_sequence`更改为什么值？
3.  Cy还指出，按照他的建议更改`eval_sequence`不会影响a部分中示例的行为。 解释为什么这是真的。
4.  您认为应该如何在惰性评估器中处理序列？ 您喜欢Cy的方法，本文中的方法还是其他方法？

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_83_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>The approach taken in this section is somewhat unpleasant, because it makes an incompatible change to JavaScript. It might be nicer to implement lazy evaluation as an _upward-compatible extension_, that is, so that ordinary JavaScript programs will work as before. We can do this by extending the syntax of function declarations to let the user control whether or not arguments are to be delayed. While we're at it, we may as well also give the user the choice between delaying with and without memoization. For example, the definition

```js
function f(a, b, c, d) {	
    parameters("strict", "lazy", "strict", "lazy-memo");
    ...
}
```

would define `f` to be a function of four arguments, where the first and third arguments are evaluated when the function is called, the second argument is delayed, and the fourth argument is both delayed and memoized. Thus, ordinary function definitions will produce the same behavior as ordinary JavaScript, while adding the `"lazy-memo"` declaration to each parameter of every compound function will produce the behavior of the lazy evaluator defined in this section. Design and implement the changes required to produce such an extension to JavaScript. You can assume that the special <quote>function call</quote> `parameters` is always the first statement in the body of a function declaration. You must also arrange for `evaluate` or `apply` to determine when arguments are to be delayed, and to force or delay arguments accordingly, and you must arrange for forcing to memoize or not, as appropriate.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_83_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise>

* * *

[[1]](83#footnote-link-1) The word _thunk_ was invented by an informal working group that was discussing the implementation of call-by-name in Algol 60\. They observed that most of the analysis of (<quote>thinking about</quote>) the expression could be done at compile time; thus, at run time, the expression would already have been <quote>thunk</quote> about (<citation>Ingerman et al. 1960</citation>).

[[2]](83#footnote-link-2) This is analogous to the use of `force` on the delayed objects that were introduced in chapter 3 to represent streams. The critical difference between what we are doing here and what we did in chapter 3 is that we are building delaying and forcing into the evaluator, and thus making this uniform and automatic throughout the language.

[[3]](83#footnote-link-3) Lazy evaluation combined with memoization is sometimes referred to as _call-by-need_ argument passing, in contrast to _call-by-name_ argument passing. (Call-by-name, introduced in Algol 60, is similar to non-memoized lazy evaluation.) As language designers, we can build our evaluator to memoize, not to memoize, or leave this an option for programmers (exercise <ref name="ex:user-controlled-strictness">[4.22](83#ex_4.22)</ref>). As you might expect from chapter 3, these choices raise issues that become both subtle and confusing in the presence of assignments. (See exercises <ref name="ex:delay-side-effects">[4.18](83#ex_4.18)</ref> and <ref name="ex:memoize-or-not">[4.20](83#ex_4.20)</ref>.) An excellent article by Clinger (1982) attempts to clarify the multiple dimensions of confusion that arise here.

[[4]](83#footnote-link-4) Notice that we also erase the `env` from the thunk once the expression's value has been computed. This makes no difference in the values returned by the interpreter. It does help save space, however, because removing the reference from the thunk to the `env` once it is no longer needed allows this structure to be _garbage-collected_ and its space recycled Similarly, we could have allowed unneeded environments in the memoized delayed objects of section <ref name="sec:delayed-lists">[3.5.1](67)</ref> to be garbage-collected, by having `memo_fun` do something like `fun = null;` to discard the function `fun` (which includes the environment in which the `delay` was evaluated) after storing its value.

[[5]](83#footnote-link-5) This exercise demonstrates that the interaction between lazy evaluation and side effects can be very confusing. This is just what you might expect from the discussion in chapter 3.

