# 3.1.3引入作业的成本

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

如我们所见，赋值语句使我们能够对具有局部状态的对象进行建模。 但是，这种优势是有代价的。 在 [1.1.5](7)良好&lt;/quote&gt;数学特性的简单模型可以成为处理编程语言中的对象和赋值的适当框架。

只要我们不使用赋值，对具有相同参数的相同函数的两次求值将产生相同的结果，因此可以将这些函数视为计算数学函数。 就像我们在本书前两章中所做的那样，不使用分配的编程被称为_函数式编程_。

要了解分配如何复杂化，请考虑 [3.1.1](49) 部分的`make_withdraw`函数的简化版本，该版本不麻烦检查不足的数量：

```js
function make_simplified_withdraw(balance) {
    return amount => {
               balance = balance - amount;
               return balance;
           };
}
```

将此功能与以下不使用分配的`make_decrementer`功能进行比较：

```js
function make_decrementer(balance) {
    return amount => balance - amount;
}
```

函数`make_decrementer`返回一个函数，该函数从指定量`balance`中减去其输入，但与`make_simplified_withdraw`一样，连续调用没有累积的影响：

```js
const d = make_decrementer(25);
```

```js
d(20); // output: 5
```

```js
d(10); // output: 15
```

我们可以使用替代模型来解释`make_decrementer`的工作原理。 例如，让我们分析表达式的评估

```js
(make_decrementer(25))(20);
```

We first simplify the operator of the combination by substituting ![25](img/e239cbaf58b35688873126f6a5d17c6a.jpg) for `balance` in the body of `make-decrementer`. This reduces the expression to

```js
(amount => 25 - amount)(20);
```

现在，通过在函数定义表达式的主体中将`amount`替换为20来应用运算符：

```js
25 - 20;
```

The final answer is 5.

但是，观察一下，如果我们尝试使用`make_simplified_withdraw`进行类似的替代分析，会发生什么：

```js
(make_simplified_withdraw(25))(20);
```

我们首先通过在`make_simplified_withdraw`的返回表达式中用[25]代替`balance`来简化运算符。 这样可以将表达式简化为 [[1]](51#footnote-1)

```js
(amount => {
     balance = 25 - amount; 
     return 25;
 })(20);
```

现在，通过在函数主体中将`amount`替换为20来应用函数：

```js
balance = 25 - 20;
return 25;
```

如果我们遵循替换模型，则必须说函数应用程序的含义是首先将`balance`设置为5，然后返回25作为表达式的值。 这得到了错误的答案。 为了获得正确的答案，我们将必须以某种方式将第一次出现的`balance`（在分配作用之前）与第二次出现的`balance`（在分配作用之后）以及替换模型 无法做到这一点。

这里的麻烦在于，替换最终基于以下观念：我们语言中的符号本质上是值的名称。 &lt;split&gt;但是，一旦我们引入赋值以及变量值可以更改的想法，变量就不再仅仅是名称。 现在，变量以某种方式指向可以存储值的位置，并且存储在该位置的值可以更改。 在 [3.2](52)位置&lt;/quote&gt;的角色。&lt;/split&gt;

## 不变与变化

这里出现的问题比仅仅特定计算模型的崩溃更为深刻。 一旦将更改引入计算模型，许多以前很简单的概念就会出现问题。 考虑两个事物&lt;quote&gt;相同的概念。&lt;/quote&gt;

假设我们使用相同的参数两次调用`make_decrementer`以创建两个函数：

```js
const d1 = make_decrementer(25);

const d2 = make_decrementer(25);
```

`d1`和`d2`是否相同？ 可以接受的答案是肯定的，因为`d1`和`d2`具有相同的计算行为-每个函数都是从25中减去其输入的函数。实际上，在任何计算中都可以用`d1`代替`d2`而无需更改 结果。

与两次调用`make_simplified_withdraw`形成对比：

```js
const w1 = make_simplified_withdraw(25);

const w2 = make_simplified_withdraw(25);
```

`w1`和`w2`是否相同？ 当然不会，因为对`w1`和`w2`的调用具有不同的效果，如以下交互序列所示：

```js
w1(20);
```

```js
w1(20);
```

```js
w2(20);
```

即使`w1`和`w2`等于，就它们两者都是通过评估相同的表达式`make_simplified_withdraw(25)`来创建的意义而言，并不是`w1`可以代替`w2` 在任何表达式中，而不会更改对表达式求值的结果。

支持 &lt;quote&gt;equals可以在不更改表达式值的情况下替换表达式中的equals&lt;/quote&gt; 的概念的语言被称为_相对透明_。 当我们在计算机语言中包含分配时，将违反参照透明性。 这使得很难确定何时可以通过替换等效表达式来简化表达式。 因此，对使用分配的程序进行推理变得更加困难。

一旦我们放弃了引用透明性，对于计算对象来说&lt;quote&gt;就是相同的&lt;/quote&gt;意味着什么，就很难以正式的方式来理解。 确实，&lt;quote&gt;与&lt;/quote&gt;在现实世界中的含义相同，即我们的程序模型本身并不十分清楚。 通常，仅通过修改一个对象，然后观察另一个对象是否以相同的方式进行更改，就可以确定两个表面上完全相同的对象确实是&lt;quote&gt;相同的对象。 但是，如何通过观察两次&lt;quote&gt;相同的&lt;/quote&gt;对象并查看该对象的某些属性是否从一次观察到另一次观察是否有所不同，来判断一个对象是否已更改？ 因此，如果没有_相同性_&lt;/quote&gt;的先验概念，我们就无法确定&lt;quote&gt;的变化&lt;/quote&gt;，并且如果不观察变化的影响就无法确定相同性。

例如，在编程中如何出现此问题的示例，请考虑Peter和Paul的银行帐户中有 &lt;dollar&gt;&lt;/dollar&gt; 100的情况。 建模为

```js
const peter_acc = make_account(100);
const paul_acc = make_account(100);
```

and modeling it as

```js
const peter_acc = make_account(100);
const paul_acc = peter_acc;
```

在第一种情况下，两个银行帐户是不同的。 Peter进行的交易不会影响Paul的帐户，反之亦然。 但是，在第二种情况下，我们将`paul_acc`定义为与`peter_acc`相同。实际上，Peter和Paul现在拥有一个联名银行帐户，并且如果Peter从`peter_acc`取款 保罗将在`paul_acc`中观察到更少的钱。 这两种相似但截然不同的情况可能会导致构建计算模型混乱。 特别是对于共享帐户，一个对象（银行帐户）具有两个不同的名称（`peter_acc`和`paul_acc`）尤其令人困惑。 如果我们在程序中搜索可以更改`paul_acc`的所有位置，则必须记住也要注意更改`peter_acc`的内容。 [[2]](51#footnote-2)

参考以上关于&lt;quote&gt;相同性&lt;/quote&gt;和&lt;quote&gt;更改的说明，&lt;/quote&gt;观察到，如果Peter和Paul只能检查他们的银行余额，而不能执行更改余额的操作，则发出 这两个帐户是否不同将是没有意义的。 通常，只要我们从不修改数据对象，就可以将复合数据对象视为其各个部分的整体。 例如，有理数通过给出其分子和分母来确定。 但是，在存在更改的情况下，该视图不再有效，因为复合数据对象的&lt;quote&gt;标识&lt;/quote&gt;与其构成的对象有所不同。 即使我们通过提款来更改余额，银行帐户仍是&lt;quote&gt;相同的&lt;/quote&gt;银行帐户； 相反，我们可以有两个具有相同状态信息的银行帐户。 这种复杂性不是由我们的编程语言引起的，而是由于我们将银行帐户视为对象的结果。 例如，我们通常不将有理数视为具有身份的可变对象，这样我们就可以更改分子，并且仍然使&lt;quote&gt;具有相同的&lt;/quote&gt;有理数。

## 指令式编程的陷阱

与功能编程相反，广泛使用分配的编程称为_命令式编程_。 除了使计算模型复杂化之外，以命令式编写的程序还容易受到功能程序中不会发生的错误的影响。 例如，从 [1.2.1](12) 部分中调用迭代阶乘程序：

```js
function factorial(n) {
   function iter(product,counter) {
      if (counter > n) {
          return product;
      } else {
          return iter(counter*product,
                       counter+1);
      }
   }
   return iter(1,1);
}
```

Instead of passing arguments in the internal iterative loop, we could adopt a more imperative style by using explicit assignment to update the values of the variables `product` and `counter`:

```js
function factorial(n) {
   let product = 1;
   let counter = 1;
   function iter() {
      if (counter > n) {
          return product;
      } else {
          product = counter * product;
          counter = counter + 1;
          return iter();
      }
   }
   return iter();
}
```

This does not change the results produced by the program, but it does introduce a subtle trap. How do we decide the order of the assignments? As it happens, the program is correct as written. But writing the assignments in the opposite order

```js
counter = counter + 1;
product = counter * product;
```

would have produced a different, incorrect result. In general, programming with assignment forces us to carefully consider the relative orders of the assignments to make sure that each statement is using the correct version of the variables that have been changed. This issue simply does not arise in functional programs.[[3]](51#footnote-3)

如果我们考虑同时执行多个进程的应用程序，则命令程序的复杂性将变得更加糟糕。 我们将在 [3.4](63) 部分中返回此内容。 但是，首先，我们将解决为涉及赋值的表达式提供计算模型的问题，并探讨在设计仿真时使用具有局部状态的对象的用途。

<exercise>Consider the bank account objects created by `make_account`, with the password modification described in exercise <ref name="ex:password-protection">[3.3](49#ex_3.3)</ref>. Suppose that our banking system requires the ability to make joint accounts. Define a function `make_joint` that accomplishes this. The function `make_joint` should take three arguments. The first is a password-protected account. The second argument must match the password with which the account was defined in order for the `make_joint` operation to proceed. The third argument is a new password. The function `make_joint` is to create an additional access to the original account using the new password. For example, if (`peter_acc` is a bank account with password (`"open sesame"`, then

```js
// make_joint function to be written by students
const paul_acc = 
    make_joint(peter_acc, "open sesame", "rosebud");
```

will allow one to make transactions on (`peter_acc` using the name `paul_acc` and the password `"rosebud"`. You may wish to modify your solution to exercise <ref name="ex:password-protection">[3.3](49#ex_3.3)</ref> to accommodate this new feature.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_51_1_div">Solution</button> <solution>```js
function make_joint(linked_acc, linked_pw, joint_pw) {
    return (message, input_pw) => {

        // Check authentication for joint account
        if (input_pw !== joint_pw) {
            return x => "Wrong joint account password";
        } else {
            const access_linked = linked_acc(message, linked_pw);

            // Check authentication for linked account
            if (access_linked(0) === "Incorrect Password") {
                // access_linked(0) does a deposit / withdrawal of 0, in order
                // to test for the "Incorrect Password" message. 
                return x => "Wrong linked account password";
            } else {
                // All authentication passed, return accessed account to user
                return access_linked;
            }
        }
    };
}
```</solution></exercise> <exercise>When we defined the evaluation model in section <ref name="sec:evaluating-combinations">[1.1.3](5)</ref>, we said that the first step in evaluating an expression is to evaluate its subexpressions. But we never specified the order in which the subexpressions should be evaluated (e.g., left to right or right to left). When we introduce assignment, the order in which the arguments to a function are evaluated can make a difference to the result. Define a simple function `f` such that evaluating `f(0) + f(1)` will return 0 if the arguments to `+` are evaluated from left to right but will return 1 if the arguments are evaluated from right to left.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_51_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]](51#footnote-link-1) We don't substitute for the occurrence of `balance` in the assignment statement because the name in an assignment is not evaluated. If we did substitute for it, we would get `25 = 25 - amount;`, which makes no sense.

[[2]](51#footnote-link-2) The phenomenon of a single computational object being accessed by more than one name is known as _aliasing_. The joint bank account situation illustrates a very simple example of an alias. In section <ref name="sec:mutable-data">[3.3](57)</ref> we will see much more complex examples, such as <quote>distinct</quote> compound data structures that share parts. Bugs can occur in our programs if we forget that a change to an object may also, as a <quote>side effect,</quote> change a <quote>different</quote> object because the two <quote>different</quote> objects are actually a single object appearing under different aliases. These so-called _side-effect bugs_ are so difficult to locate and to analyze that some people have proposed that programming languages be designed in such a way as to not allow side effects or aliasing (<citation>Lampson et al. 1981</citation>; <citation>Morris, Schmidt, and Wadler 1980</citation>).

[[3]](51#footnote-link-3) In view of this, it is ironic that introductory programming is most often taught in a highly imperative style. This may be a vestige of a belief, common throughout the 1960s and 1970s, that programs that call functions must inherently be less efficient than programs that perform assignments. ( <citation>斯蒂尔（1977）

Steele 1977</citation> debunks this argument.) Alternatively it may reflect a view that step-by-step assignment is easier for beginners to visualize than function call. Whatever the reason, it often saddles beginning programmers with <quote>should I set this variable before or after that one</quote> concerns that can complicate programming and obscure the important ideas.

