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

3.1.3 The Costs of Introducing Assignment

As we have seen, the set! operation enables us to model objects that

have local state. However, this advantage comes at a price. Our programming

language can no longer be interpreted in terms of the substitution model of

procedure application that we introduced in 1.1.5. Moreover, no

simple model with “nice” mathematical properties can be an adequate framework

for dealing with objects and assignment in programming languages.

正如我们所看到的,`set!` 操作使我们能够对具有局部状态的对象建模。然而,这一优势是有代价的。我们的程序设计语言不再能够用 1.1.5 节引入的过程应用代换模型来解释。此外,任何具有“良好”数学性质的简单模型,都不足以作为处理程序设计语言中对象与赋值问题的适当框架。

So long as we do not use assignments, two evaluations of the same procedure

with the same arguments will produce the same result, so that procedures can be

viewed as computing mathematical functions. Programming without any use of

assignments, as we did throughout the first two chapters of this book, is

accordingly known as

functional programming.

只要我们不使用赋值,对同一过程以相同参数进行的两次求值将产生相同的结果,因而过程可以被视为在计算数学函数。我们在本书前两章中始终如此编程,从未使用赋值,这种风格相应地被称为函数式程序设计 (functional programming)。

To understand how assignment complicates matters, consider a simplified version

of the make-withdraw procedure of 3.1.1 that does not

bother to check for an insufficient amount:

为了理解赋值如何使问题复杂化,考虑 3.1.1 节 `make-withdraw` 过程的一个简化版本,它省去了对余额不足情况的检查:

Compare this procedure with the following make-decrementer procedure,

which does not use set!:

将这个过程与下面不使用 `set!` 的 `make-decrementer` 过程对比:

Make-decrementer returns a procedure that subtracts its input from a

designated amount balance, but there is no accumulated effect over

successive calls, as with make-simplified-withdraw:

`make-decrementer` 返回一个过程,该过程从指定金额 `balance` 中减去其输入,但与 `make-simplified-withdraw` 不同,对它的连续调用不会产生累积效果:

We can use the substitution model to explain how make-decrementer works.

For instance, let us analyze the evaluation of the expression

我们可以用代换模型来解释 `make-decrementer` 的工作方式。例如,让我们分析以下表达式的求值过程:

We first simplify the operator of the combination by substituting 25 for

balance in the body of make-decrementer. This reduces the

expression to

首先,我们通过在 `make-decrementer` 的体中将 `balance` 替换为 25 来化简组合式的运算符,从而将表达式化简为:

Now we apply the operator by substituting 20 for amount in the body of

the lambda expression:

然后,我们在 lambda 表达式的体中将 `amount` 替换为 20 来应用该运算符:

The final answer is 5.

最终结果为 5。

Observe, however, what happens if we attempt a similar substitution analysis

with make-simplified-withdraw:

然而,请注意,当我们试图对 `make-simplified-withdraw` 进行类似的代换分析时,会发生什么:

We first simplify the operator by substituting 25 for balance in the

body of make-simplified-withdraw. This reduces the expression

to

首先,我们在 `make-simplified-withdraw` 的体中将 `balance` 替换为 25 来化简运算符,从而将表达式化简为:

Now we apply the operator by substituting 20 for amount in the body of

the lambda expression:

然后,我们在 lambda 表达式的体中将 `amount` 替换为 20 来应用该运算符:

If we adhered to the substitution model, we would have to say that the meaning

of the procedure application is to first set balance to 5 and then

return 25 as the value of the expression. This gets the wrong answer. In

order to get the correct answer, we would have to somehow distinguish the first

occurrence of balance (before the effect of the set!) from the

second occurrence of balance (after the effect of the set!), and

the substitution model cannot do this.

如果我们恪守代换模型,就必须认为该过程应用的含义是:先将 `balance` 置为 5,然后将 25 作为表达式的值返回。这会得出错误的答案。为了得到正确答案,我们必须以某种方式区分 `balance` 的第一次出现(`set!` 生效之前)和第二次出现(`set!` 生效之后),而代换模型做不到这一点。

The trouble here is that substitution is based ultimately on the notion that

the symbols in our language are essentially names for values. But as soon as

we introduce set! and the idea that the value of a variable can change,

a variable can no longer be simply a name. Now a variable somehow refers to a

place where a value can be stored, and the value stored at this place can

change. In 3.2 we will see how environments play this role of

“place” in our computational model.

Subheading: Sameness and change

问题的根源在于,代换归根结底基于这样一个概念:我们语言中的符号本质上是值的名字。但是,一旦引入 `set!` 以及变量的值可以改变这一思想,变量就不再仅仅是一个名字了。变量现在以某种方式指向一个可以存储值的位置,而存储在该位置的值是可以改变的。在 3.2 节中,我们将看到环境 (environment) 如何在我们的计算模型中扮演这个“位置”的角色。\n小标题:同一性与变化

The issue surfacing here is more profound than the mere breakdown of a

particular model of computation. As soon as we introduce change into our

computational models, many notions that were previously straightforward become

problematical. Consider the concept of two things being “the same.”

这里浮现的问题远比某个特定计算模型的失效更为深刻。一旦我们向计算模型中引入变化,许多此前显而易见的概念便变得棘手起来。试想“两个事物相同”这一概念。

Suppose we call make-decrementer twice with the same argument to create

two procedures:

假设我们用相同的参数调用 `make-decrementer` 两次,创建两个过程:

Are D1 and D2 the same? An acceptable answer is yes, because

D1 and D2 have the same computational behavior—each is a

procedure that subtracts its input from 25. In fact, D1 could be

substituted for D2 in any computation without changing the result.

Contrast this with making two calls to make-simplified-withdraw:

`D1` 和 `D2` 相同吗?一个可以接受的答案是肯定的,因为 `D1` 和 `D2` 具有相同的计算行为——两者都是从 25 中减去输入的过程。事实上,在任何计算中用 `D1` 代替 `D2`,结果都不会改变。再来看对 `make-simplified-withdraw` 的两次调用:

Are W1 and W2 the same? Surely not, because calls to W1

and W2 have distinct effects, as shown by the following sequence of

interactions:

`W1` 和 `W2` 相同吗?显然不同,因为对 `W1` 和 `W2` 的调用会产生不同的效果,如下面的交互序列所示:

Even though W1 and W2 are “equal” in the sense that they are

both created by evaluating the same expression, (make-simplified-withdraw

25), it is not true that W1 could be substituted for W2 in any

expression without changing the result of evaluating the expression.

尽管 `W1` 和 `W2` 在某种意义上是“相等的”——它们都是通过对同一表达式 `(make-simplified-withdraw 25)` 求值而创建的——但在任何表达式中用 `W1` 代替 `W2` 并不能保证求值结果不变。

A language that supports the concept that “equals can be substituted for

equals” in an expression without changing the value of the expression is said

to be

referentially transparent. Referential transparency is

violated when we include set! in our computer language. This makes it

tricky to determine when we can simplify expressions by substituting equivalent

expressions. Consequently, reasoning about programs that use assignment

becomes drastically more difficult.

一门支持“等量代换等量不改变表达式的值”这一概念的语言,被称为引用透明的 (referentially transparent)。当我们在计算机语言中引入 `set!` 时,引用透明性就被破坏了。这使得判断何时可以通过用等价表达式替换来化简表达式变得困难重重。因此,对使用赋值的程序进行推理会变得极为困难。

Once we forgo referential transparency, the notion of what it means for

computational objects to be “the same” becomes difficult to capture in a

formal way. Indeed, the meaning of “same” in the real world that our

programs model is hardly clear in itself. In general, we can determine that

two apparently identical objects are indeed “the same one” only by modifying

one object and then observing whether the other object has changed in the same

way. But how can we tell if an object has “changed” other than by observing

the “same” object twice and seeing whether some property of the object

differs from one observation to the next? Thus, we cannot determine “change”

without some a priori notion of “sameness,” and we cannot determine

sameness without observing the effects of change.

一旦我们放弃引用透明性,“计算对象相同”这一概念便很难以形式化的方式加以把握。事实上,我们的程序所建模的现实世界中“相同”的含义本身也并不清晰。一般而言,我们只能通过修改一个对象,然后观察另一个对象是否以同样的方式发生变化,才能确定两个表面上完全相同的对象是否真的“是同一个”。但我们又怎能判断一个对象是否“发生了变化”?除非我们两次观察“同一个”对象,并比较某个属性在两次观察之间是否有所不同。因此,若没有某种先验的“同一性”概念,我们便无法判断“变化”;而若没有观察变化的效果,我们又无法确定同一性。

As an example of how this issue arises in programming, consider the situation

where Peter and Paul have a bank account with $100 in it. There is a

substantial difference between modeling this as

作为这一问题在程序设计中如何显现的例子,考虑这样一种情形:Peter 和 Paul 共用一个存有 $100 的银行账户。将其建模为:

and modeling it as

与将其建模为:

In the first situation, the two bank accounts are distinct. Transactions made

by Peter will not affect Paul’s account, and vice versa. In the second

situation, however, we have defined paul-acc to be the same thing

as peter-acc. In effect, Peter and Paul now have a joint bank account,

and if Peter makes a withdrawal from peter-acc Paul will observe less

money in paul-acc. These two similar but distinct situations can cause

confusion in building computational models. With the shared account, in

particular, it can be especially confusing that there is one object (the bank

account) that has two different names (peter-acc and paul-acc);

if we are searching for all the places in our program where paul-acc can

be changed, we must remember to look also at things that change

peter-acc.

在第一种情形中,两个银行账户是相互独立的。Peter 的操作不会影响 Paul 的账户,反之亦然。然而在第二种情形中,我们将 `paul-acc` 定义为与 `peter-acc` 同一个东西。实际上,Peter 和 Paul 现在拥有一个联名账户:如果 Peter 从 `peter-acc` 取款,Paul 就会发现 `paul-acc` 中的钱少了。这两种相似却不同的情形在建立计算模型时极易引发混乱。对于共享账户,尤其令人困惑的是:一个对象(银行账户)拥有两个不同的名字(`peter-acc` 和 `paul-acc`)。如果我们想在程序中搜索所有可能改变 `paul-acc` 的地方,就必须记住同时查看那些改变 `peter-acc` 的地方。

With reference to the above remarks on “sameness” and “change,” observe

that if Peter and Paul could only examine their bank balances, and could not

perform operations that changed the balance, then the issue of whether the two

accounts are distinct would be moot. In general, so long as we never modify

data objects, we can regard a compound data object to be precisely the totality

of its pieces. For example, a rational number is determined by giving its

numerator and its denominator. But this view is no longer valid in the

presence of change, where a compound data object has an “identity” that is

something different from the pieces of which it is composed. A bank account is

still “the same” bank account even if we change the balance by making a

withdrawal; conversely, we could have two different bank accounts with the same

state information. This complication is a consequence, not of our programming

language, but of our perception of a bank account as an object. We do not, for

example, ordinarily regard a rational number as a changeable object with

identity, such that we could change the numerator and still have “the same”

rational number.

Subheading: Pitfalls of imperative programming

结合上文关于“同一性”与“变化”的论述,可以注意到:如果 Peter 和 Paul 只能查看各自的账户余额,而不能执行改变余额的操作,那么两个账户是否独立的问题便无关紧要了。一般而言,只要我们从不修改数据对象,就可以将一个复合数据对象准确地视为其所有组成部分的总和。例如,一个有理数由其分子和分母唯一确定。但一旦存在变化,这种观点便不再成立——此时复合数据对象拥有一个“标识 (identity)”,它有别于其各个组成部分。即便通过取款改变了余额,一个银行账户依然是“同一个”账户;反之,两个不同的银行账户也可以拥有完全相同的状态信息。这一复杂性并非来源于我们的程序设计语言,而是来源于我们将银行账户视为对象的认知方式。例如,我们通常不会将有理数视为具有标识的可变对象——不会认为改变了分子之后,它仍然是“同一个”有理数。\n小标题:命令式程序设计的陷阱

In contrast to functional programming, programming that makes extensive use of

assignment is known as

imperative programming. In addition to

raising complications about computational models, programs written in

imperative style are susceptible to bugs that cannot occur in functional

programs. For example, recall the iterative factorial program from

1.2.1:

与函数式程序设计相对,大量使用赋值的程序设计被称为命令式程序设计 (imperative programming)。除了在计算模型上引入复杂性之外,以命令式风格编写的程序还容易出现函数式程序中根本不会发生的程序错误。例如,回顾 1.2.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:

我们不必在内部迭代循环中传递参数,而可以采用更具命令式风格的写法——用显式赋值来更新变量 `product` 和 `counter` 的值:

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

这并不改变程序产生的结果,但确实引入了一个微妙的陷阱。我们应该如何决定赋值的顺序?恰好,按原文所写的顺序,程序是正确的。但若将赋值顺序颠倒:

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.

就会产生不同的、错误的结果。一般而言,使用赋值进行程序设计迫使我们仔细考量各条赋值语句的相对顺序,以确保每条语句都在使用正确版本的已变更变量。这一问题在函数式程序中根本不会出现。

The complexity of imperative programs becomes even worse if we consider

applications in which several processes execute concurrently. We will return

to this in 3.4. First, however, we will address the issue of

providing a computational model for expressions that involve assignment, and

explore the uses of objects with local state in designing simulations.

如果再考虑多个计算过程并发执行的应用场景,命令式程序的复杂性会变得更加严峻。我们将在 3.4 节中回到这一问题。不过,在此之前,我们将先处理为涉及赋值的表达式提供计算模型这一问题,并探索在设计模拟程序时如何利用具有局部状态的对象。

Racket #lang sicp
(define (make-simplified-withdraw balance)
 (lambda (amount)
 (set! balance (- balance amount))
 balance))

(define W (make-simplified-withdraw 25))

(W 20)
5

(W 10)
-5
Racket #lang sicp
(define (make-decrementer balance)
 (lambda (amount)
 (- balance amount)))
Racket #lang sicp
(define D (make-decrementer 25))

(D 20)
5

(D 10)
15
Racket #lang sicp
((make-decrementer 25) 20)
Racket #lang sicp
((lambda (amount) (- 25 amount)) 20)
Racket #lang sicp
(- 25 20)
Racket #lang sicp
((make-simplified-withdraw 25) 20)
Racket #lang sicp
((lambda (amount)
 (set! balance (- 25 amount)) 25)
 20)
Racket #lang sicp
(set! balance (- 25 20)) 25
Racket #lang sicp
(define D1 (make-decrementer 25))
(define D2 (make-decrementer 25))
Racket #lang sicp
(define W1 (make-simplified-withdraw 25))
(define W2 (make-simplified-withdraw 25))
Racket #lang sicp
(W1 20)
5

(W1 20)
-15

(W2 20)
5
Racket #lang sicp
(define peter-acc (make-account 100))
(define paul-acc (make-account 100))
Racket #lang sicp
(define peter-acc (make-account 100))
(define paul-acc peter-acc)
Racket #lang sicp
(define (factorial n)
 (define (iter product counter)
 (if (> counter n)
 product
 (iter (* counter product)
 (+ counter 1))))
 (iter 1 1))
Racket #lang sicp
(define (factorial n)
 (let ((product 1)
 (counter 1))
 (define (iter)
 (if (> counter n)
 product
 (begin (set! product (* counter
 product))
 (set! counter (+ counter 1))
 (iter))))
 (iter)))
Racket #lang sicp
(set! counter (+ counter 1))
(set! product (* counter product))