# 4.3.3实施环境评估者

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

普通JavaScript表达式的求值可能返回一个值，可能永不终止，或者可能表示错误。 在不确定的JavaScript中，对表达式的求值可能会另外导致死胡同的发现，在这种情况下，求值必须回溯到先前的选择点。 这种不确定的情况使非确定性JavaScript的解释变得复杂。

我们将通过修改 [4.1.7](80) 部分的分析评估器来构造用于不确定性JavaScript的`amb`评估器。 [[1]](88#footnote-1) 与分析评估器一样，通过调用由对该表达式的分析产生的执行函数来完成表达式的评估。 普通JavaScript的解释和非确定性JavaScript的解释之间的差异将完全在执行功能上。

## 执行功能和延续

回想一下，普通评估器的执行函数采用一个参数：执行环境。 相反，`amb`评估程序中的执行函数采用三个参数：环境和两个称为_延续函数_的函数。 表达式的求值将通过调用以下两个延续之一来完成：如果求值得出一个值，则使用该值调用_成功延续_； 如果评估导致发现死角，则调用_故障继续_。 构造和调用适当的延续是不确定性评估程序执行回溯的机制。

接收值并继续进行计算是成功继续的工作。 成功延续值与该值一起传递给另一个失败延续，如果该值的使用导致死胡同，则随后将继续调用该失败延续。

尝试非确定性过程的另一个分支是失败继续的工作。 非确定性语言的本质在于，表达式可以代表备选方案中的选择。 即使事先不知道哪种选择会导致可接受的结果，也必须使用指示的替代选择之一对这种表达式进行评估。 为了解决这个问题，评估人员选择了其中一种方法，并将该值传递给成功的延续。 评估程序与该值一起构造并传递故障继续，以后可以调用该失败继续以选择其他替代方案。

当用户程序明确拒绝当前攻击行（例如，对`require`的调用可能会导致执行`amb()`时，该评估始终在评估过程中触发失败（即调用失败继续）） 失败-请参阅 [4.3.1](86) 一节）。 此时发生的故障继续将导致最近的选择点选择另一种选择。 如果在该选择点不再需要考虑其他选择，则会触发较早选择点的故障，依此类推。 驱动程序循环还响应`try-again`请求调用故障继续，以查找表达式的另一个值。

此外，如果在选择导致的过程的分支上发生了副作用操作（例如，对变量的赋值），则当过程发现死胡同时，可能有必要先消除副作用，然后再进行选择。 新的选择。 这是通过使副作用操作产生故障延续来实现的，该故障延续消除了副作用并传播了故障。

总而言之，故障延续是由

*   `amb`表达式-提供一种机制，以使`amb`表达式的当前选择导致死胡同；
*   顶层驱动程序—提供一种机制，可以在选择用尽时报告失败；
*   分配-拦截回溯过程中的失败并撤消分配。

仅当遇到死胡同时才启动故障。 发生这种情况

*   如果用户程序执行`amb()`；
*   如果用户在顶级驱动程序上键入`try-again`。

故障继续在故障处理期间也称为：

*   当分配创建的故障继续完成消除副作用时，它将调用其拦截的故障继续，以将故障传播回导致该分配的选择点或顶级。
*   当`amb`的故障延续用尽选项时，它将调用最初提供给`amb`的故障延续，以将故障传播回先前的选择点或顶层。

## 评估者的结构

`amb`评估程序的语法和数据表示功能，以及基本的`analyze`函数，与 [4.1.7](80) 部分的评估程序相同。 因为我们需要其他语法功能来识别`amb`表达式： [[2]](88#footnote-2)

```js
function is_amb(stmt) {      
    return is_tagged_list(stmt, "application") && 
           is_name(operator(stmt)) && 
           name_of_name(operator(stmt)) === "amb";
}
function amb_choices(stmt) {
    return operands(stmt);
}
```

我们还必须在`analyze`中的分派中添加一个子句，该子句将识别此类表达式并生成适当的执行函数：

```js
: is_amb(stmt)
  ? analyze_amb(stmt)
```

顶层函数`ambeval`（类似于 [4.1.7](80) 部分中提供的`evaluate`版本）分析给定表达式并将结果执行函数应用于给定环境 ，以及两个给定的延续：

```js
function ambeval(exp, env, succeed, fail) {      
    return analyze(exp)(env, succeed, fail);
}
```

成功延续是两个参数的函数：刚刚获得的值和如果该值导致后续失败，则使用另一个失败延续。 故障继续是无参数的函数。 所以执行函数的一般形式是

```js
(env, succeed, fail) => {      
    // succeed is a function of the form: (value, fail) => ...
    // fail    is a function of the form: is () => ...
}
```

For example, executing `ambeval(![%5Ctextit%7Bexp%7D](img/5b76a7a8510eec5721b07de8ebab9fe6.jpg), the_global_environment, (value, fail) => value, () => "failed");` will attempt to evaluate the given expression and will return either the expression's value (if the evaluation succeeds) or the string `"failed"` (if the evaluation fails). The call to `ambeval` in the driver loop shown below uses much more complicated continuation functions, which continue the loop and support the `try-again` request.

`amb`评估器的大多数复杂性是由于执行函数相互调用时传递连续性的机制而导致的。 通过下面的代码，您应该将每个执行功能与 [4.1.7](80) 部分中给出的普通评估器的对应功能进行比较。

## 简单表达

除了需要管理连续性之外，最简单类型的表达式的执行功能与普通评估器的执行功能基本相同。 执行函数仅使用表达式的值成功，并传递给它们的失败继续。

```js
function analyze_self_evaluating(stmt) {
    return (env, succeed, fail) => succeed(stmt, fail);
}

function analyze_name(stmt) {
    return (env, succeed, fail) =>
           succeed(lookup_name_value(name_of_name(stmt), env),
                   fail);
}

function analyze_function_definition(stmt) {
    const vars = 
        function_definition_parameters(stmt);
    const bfun =
        analyze(function_definition_body(stmt));
    return (env, succeed, fail) =>
        succeed(make_function(vars, bfun, env),
                fail);
}
```

注意，查找名称总是&lt;quote&gt;成功。&lt;/quote&gt; 如果`lookup_name_value`找不到名称，则照常发出错误信号。 此类&lt;quote&gt;错误&lt;/quote&gt;表示程序错误-对未绑定变量的引用； 这并不表示我们应该尝试另一种不确定性选择，而不是当前正在尝试的选择。

## 条件和顺序

条件句的处理方式也与普通评估器中的处理方式类似。 由`analyze_conditional_expression`生成的执行函数以成功继续调用谓词执行函数`pfun`，该继续执行检查谓词值是否为true，然后继续执行结果项或替代项。 如果`pfun`的执行失败，则将调用条件表达式的原始失败继续。

```js
function analyze_conditional_expression(stmt) {      
    const pfun = analyze(cond_expr_pred(stmt));
    const cfun = analyze(cond_expr_cons(stmt));
    const afun = analyze(cond_expr_alt(stmt));
    return (env, succeed, fail) =>
           pfun(env,
                // success continuation for evaluating the predicate
                // to obtain pred_value
                (pred_value, fail2) =>
                    is_true(pred_value) 
                    ? cfun(env, succeed, fail2)
                    : afun(env, succeed, fail2),
                fail);
}
```

除了传递连续性所需的子功能`sequentially`中的机加工外，序列的处理方式也与先前的评估器相同。 即，为了依次执行`a`然后执行`b`，我们调用`a`并成功调用`b`。

```js
function analyze_sequence(stmts) {
    function sequentially(a, b) {
        return (env, succeed, fail) => 
                   a(env, 
		     (a_value, fail2) => b(env, succeed, fail2),
                     fail);
    }
    function loop(first_fun, rest_funs) {
        return is_null(rest_funs)
               ? first_fun
               : loop(sequentially(first_fun,
                          head(rest_funs)),
                      tail(rest_funs));
    }
    const funs = map(analyze, stmts);
    return is_null(funs) 
           ? env => undefined
           : loop(head(funs), tail(funs));
}
```

## 声明和转让

声明是另一种情况，我们必须为管理连续性而费心，因为在实际定义新变量之前必须评估定义值表达式。 为此，通过环境，成功继续和失败继续调用声明值执行函数`vfun`。 如果`vfun`的执行成功，则为声明的名称获取值`val`，则声明该名称并传播成功：

```js
function analyze_variable_declaration(stmt) {	   
    const name = variable_declaration_name(stmt);
    const vfun = variable_declaration_value(stmt);
    return (env, succeed, fail) => 
              vfun(env,
                   (val, fail2) => {
                       declare_variable(name, val, env);
                       succeed("ok", fail2);
                   },
	           fail);
}
function analyze_constant_declaration(stmt) {	   
    const name = 
        constant_declaration_name(stmt);
    const vfun =
        constant_declaration_value(stmt);
    return (env, succeed, fail) => 
              vfun(env,
                   (val, fail2) => {
                       declare_constant(name, val, env);
                       succeed("ok", fail2);
                   },
	           fail);
}
```

作业更有趣。 这是我们真正使用延续而不是仅仅传递延续的第一个地方。 分配的执行功能就像定义的功能一样开始。 它首先尝试获取要分配给变量的新值。 如果对`vfun`的评估失败，则分配失败。

但是，如果`vfun`成功，然后继续进行分配，则必须考虑到该计算分支稍后可能失败的可能性，这将要求我们回溯到分配之外。 因此，在回溯过程中，我们必须安排撤消分配。 [[3]](88#footnote-3)

这是通过给`vfun`成功继续执行（在下面用注释 &lt;quote&gt;* 1 *&lt;/quote&gt; 标记）来完成的，该继续操作在将新值分配给变量并从赋值开始之前保存了变量的旧值。 与分配的值一起传递的故障继续（在下面用注释 &lt;quote&gt;* 2 *&lt;/quote&gt; 标记）会在继续故障之前恢复变量的旧值。 也就是说，成功的分配提供了故障继续，将拦截后续的故障。 否则任何失败的调用`fail2`都会调用此函数，以在实际调用`fail2`之前撤消分配。

```js
function analyze_assignment(stmt) {
    const variable = assignment_variable(stmt);
    const vfun = analyze(assignment_value(stmt));
    return (env, succeed, fail) =>
           vfun(env,
                (val, fail2) => { // *1*
                    const old_value = lookup_name_value(variable, env);
                    set_variable_value(variable, val, env);
	            succeed("ok",
                            () => { // *2*
                                set_variable_value(variable, old_value, env);
                                fail2();
                            });
                },
		fail);
}
```

## 功能应用

应用程序的执行功能除了管理连续性的技术复杂性外，没有其他新思想。 由于需要在评估操作数时跟踪成功和失败的继续，因此`analyze_application`中出现了这种复杂性。 我们使用函数`get_args`来评估操作数列表，而不是像普通评估器中那样简单地使用`map`。

```js
function analyze_application(stmt) {	
    const ffun = analyze(operator(stmt));
    const afuns = map(analyze, operands(stmt));
    return (env, succeed, fail) =>
               ffun(env,
                    (fun, fail2) =>
                        get_args(afuns, 
                                 env,
   			         (args, fail3) =>
                                     execute_application(fun,
                                         args, succeed, fail3),
				 fail2),
		    fail); 
}
```

在`get_args`中，请注意`tail`如何在`afun`列表中向下执行，执行功能和`pair`在`args`的结果列表中如何通过成功调用该列表中的每个`afun`来完成。 递归调用`get_args`。 这些对`get_args`的递归调用中的每一个都有一个成功的延续，其值是新获得的参数到累积参数列表上的`pair`：

```js
function get_args(afuns, env, succeed, fail) {      
    return is_null(afuns)
        ? succeed(null, fail)
        : head(afuns)(env,
                      // success continuation for this afun
	              (arg, fail2) =>
		          get_args(tail(afuns),
                                   env,
                                   // success continuation for
				   // recursive call to get_args
				   (args, fail3) =>
				       succeed(pair(arg, args),
				               fail3),
			           fail2),
		      fail);
}
```

由`execute_application`执行的实际功能应用程序与普通评估程序的实现方式相同，但需要管理延续性。

```js
function execute_application(fun, args, succeed, fail) {
    return is_primitive_function(fun)
        ? succeed(apply_primitive_function(fun, args),
                  fail)
        : is_compound_function(fun) 
        ? function_body(fun)(
              extend_environment(
                  function_parameters(fun),
                  args,
                  function_environment(fun)),
              succeed,       
              fail)
        : error(fun, "unknown function type in " +
                     "execute_application");
}
```

## 评估`amb`表达式

`amb`表达式是不确定性语言中的关键元素。 在这里，我们看到了解释过程的本质以及跟踪延续的原因。 `amb`的执行函数定义了一个循环`try_next`，该循环为`amb`表达式的所有可能值循环执行函数。 调用每个执行函数都会失败，并继续尝试下一个失败函数。 当没有其他选择尝试时，整个`amb`表达式将失败。

```js
function analyze_amb(exp) {
    const cfuns = map(analyze, amb_choices(exp));
    return (env, succeed, fail) => {
        function try_next(choices) {
            return is_null(choices)
                ? fail()
                : head(choices)(env,
		                succeed,
                                () =>
    	                        try_next(tail(choices)));
        }
        return try_next(cfuns);
    };
}
```

## 驱动循环

`amb`评估程序的驱动程序循环很复杂，这是由于允许用户再次尝试评估表达式的机制所致。 驱动程序使用名为`internal_loop`的函数，该函数将函数`try_again`作为参数。 目的是在不确定性评估中调用`try_again`继续进行下一个未尝试的选择。 函数`internal_loop`响应于用户在驱动程序循环中键入`try-again`来调用`try_again`，或者通过调用`ambeval`开始新的评估。

对该`ambeval`调用的失败继续通知用户没有更多值，并重新调用驱动程序循环。

调用`ambeval`的成功继续更加微妙。 我们打印获得的值，然后使用`try_again`函数再次调用内部循环，该函数将能够尝试下一个替代方法。 此`next_alternative`函数是传递给成功继续的第二个参数。 通常，我们认为第二个参数是如果当前评估分支稍后失败时将使用的失败继续。 但是，在这种情况下，我们已经完成了成功的评估，因此我们可以调用&lt;quote&gt;失败&lt;/quote&gt;替代分支，以便搜索其他成功的评估。

```js
const input_prompt = "// Amb-Eval input:";
const output_prompt =  "// Amb-Eval value:";
function driver_loop() {
    function internal_loop(try_again) {
        prompt_for_input(input_prompt);
        const input = read();
        if (input === "try-again") {
            try_again();
        } else {
            display("// Starting a new problem ");
            ambeval(input,
                the_global_environment,
                // ambeval success
                (val, next_alternative) => {
                    announce_output(output_prompt);
                    user_print(val);
                    return internal_loop(next_alternative);
                },
		// ambeval failure
                () => {
                    announce_output(
                        "// There are no more values of");
                    user_print(input);
                    return driver_loop();
                });
        }
    }
    return internal_loop(
               () => {
                   display("// There is no current problem");
                   return driver_loop();
               });
}
```

对`internal_loop`的初始调用使用`try_again`函数，该函数抱怨当前没有问题，并重新启动驱动程序循环。 如果用户在没有进行任何评估的情况下键入`try-again`，就会发生这种情况。

<exercise>Implement a new special form `ramb` that is like `amb` except that it searches alternatives in a random order, rather than from left to right. Show how this can help with Alyssa's problem in exercise <ref name="ex:sentence-generate">[4.40](87#ex_4.40)</ref>.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_88_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>Implement a new kind of assignment that is not undone upon failure. For example, we can choose two distinct elements from a list and count the number of trials required to make a successful choice as follows:

```js
let count = 0;

let x = an_element_of("a", "b", "c");
let y = an_element_of("a", "b", "c");
count = count + 1;
require(! x === y);
list(x, y, count);

// Starting a new problem
// Amb-Eval value:
["a", ["b", [2, null]]]

// Amb-Eval input:
try-again
// Amb-Eval value:
["a", ["c", [3, null]]]
```

What values would have been displayed if we had used assignment as originally defined here rather than the new kind?<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_88_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>Implement a new kind of expression `if_fail` that permits the user to catch the failure of an expression. An application of `if_fail` to two expressions evaluates the first one as usual, and returns as usual if the evaluation succeeds. If the evaluation fails, however, the value of the second expression is returned, as in the following example:

```js
// Amb-Eval input:      
if_fail( ( x => {
               require(is_even(x));
               return x;
           } )(an_element_of(1, 3, 5)),
         "all-odd");
// Starting a new problem	   
// Amb-Eval value:
"all-odd"
// Amb-Eval input:
if_fail( ( x => {
               require(is_even(x));
               return x;
           } )(an_element_of(1, 3, 5, 8)),
         "all-odd");
// Starting a new problem	   
// Amb-Eval value:
8
```

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_88_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>With the new kind of assignment as described in exercise <ref name="ex:permanent-set">[4.42](88#ex_4.42)</ref> and `if_fail` as in exercise <ref name="ex:if-fail">[4.43](88#ex_4.43)</ref>, what will be the result of evaluating

```js
let pairs = null;      
if_fail( ( p => {
               pairs = pair(p, pairs);
               amb();
           } )(prime_sum_pair(list(1, 3, 5, 8), list(20, 35, 110))),
         pairs);
```

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_88_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>If we had not realized that `require` could be implemented as an ordinary function that uses `amb`, to be defined by the user as part of a nondeterministic program, we would have had to implement it in a way similar to the way we implemented `amb`. This would require syntax functions

```js
function is_require(stmt) {
    return is_tagged_list(stmt, "application") && 
           is_name(operator(stmt)) && 
           name_of_name(operator(stmt)) === "require";
}
function require_predicate(exp) {
    return operands(exp);
}
```

and a new clause in the dispatch in `analyze`

```js
: is_require(stmt)
  ? analyze_require(stmt)
```

as well the function `analyze-require` that handles `require` expressions. Complete the following definition of `analyze-require`.

```js
function analyze_require(stmt) {      
    return (env, succeed, fail) =>
        pfun(env,
             (pred_value, fail2) =>
                 ... 
	         ? ...
                 : succeed("ok", fail2),
             fail);
}
```

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_88_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]](88#footnote-link-1) We chose to implement the lazy evaluator in section <ref name="sec:lazy-evaluation">[4.2](81)</ref> as a modification of the ordinary metacircular evaluator of section <ref name="sec:core-of-evaluator">[4.1.1](74)</ref>. In contrast, we will base the `amb` evaluator on the analyzing evaluator of section <ref name="sec:separating-analysis">[4.1.7](80)</ref>, because the execution functions in that evaluator provide a convenient framework for implementing backtracking.

[[2]](88#footnote-link-2) We assume that the evaluator supports `let` (see exercise <ref name="ex:implement-analyze-let">**Cound not find label for ex:implement-analyze-let**</ref>), which we have used in our nondeterministic programs.

[[3]](88#footnote-link-3) We didn't worry about undoing declarations, since we can assume that internal declarations are scanned out (section <ref name="sec:internal-definitions">[4.1.6](79)</ref>).

