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

3.1.2 The Benefits of Introducing Assignment

As we shall see, introducing assignment into our programming language leads us

into a thicket of difficult conceptual issues. Nevertheless, viewing systems

as collections of objects with local state is a powerful technique for

maintaining a modular design. As a simple example, consider the design of a

procedure rand that, whenever it is called, returns an integer chosen at

random.

正如我们将看到的,在编程语言中引入赋值会将我们带入一片充满艰深概念问题的丛林。然而,将系统视为具有局部状态的对象的集合,是维护模块化设计的有力技术。以一个简单的例子为证:考虑设计一个过程 rand,它每次被调用时都返回一个随机选取的整数。

It is not at all clear what is meant by “chosen at random.” What we

presumably want is for successive calls to rand to produce a sequence of

numbers that has statistical properties of uniform distribution. We will not

discuss methods for generating suitable sequences here. Rather, let us assume

that we have a procedure rand-update that has the property that if we

start with a given number x

1 and form

"随机选取"究竟是什么意思,并不十分清楚。我们大概希望连续调用 rand 能产生一个具有均匀分布统计性质的数列。我们不在这里讨论生成合适序列的方法,而是假定我们有一个过程 rand-update,它具有如下性质:如果从某个给定数 x

1 出发,并构成

then the sequence of values x

1, x

2, x

3, … will have the

desired statistical properties.

那么值序列 x

1,x

2,x

3,… 将具有所需的统计性质。

We can implement rand as a procedure with a local state variable

x that is initialized to some fixed value random-init. Each call

to rand computes rand-update of the current value of x,

returns this as the random number, and also stores this as the new value of

x.

我们可以将 rand 实现为一个带有局部状态变量 x 的过程,x 被初始化为某个固定值 random-init。每次调用 rand 时,都计算当前 x 值的 rand-update,将其作为随机数返回,同时将其存储为 x 的新值。

Of course, we could generate the same sequence of random numbers without using

assignment by simply calling rand-update directly. However, this would

mean that any part of our program that used random numbers would have to

explicitly remember the current value of x to be passed as an argument

to rand-update. To realize what an annoyance this would be, consider

using random numbers to implement a technique called

Monte Carlo simulation.

当然,不使用赋值也可以生成同样的随机数序列——只需直接调用 rand-update 即可。然而,这样一来,程序中使用随机数的任何部分都必须显式地记住 x 的当前值,以便将其作为参数传递给 rand-update。为了体会这有多麻烦,不妨考虑用随机数来实现一种称为蒙特卡罗模拟 (Monte Carlo simulation) 的技术。

The Monte Carlo method consists of choosing sample experiments at random from a

large set and then making deductions on the basis of the probabilities

estimated from tabulating the results of those experiments. For example, we

can approximate π using the fact that 6

蒙特卡罗方法由以下做法构成:从一个大的集合中随机选取样本实验,然后根据对这些实验结果进行统计所估算出的概率作出推断。例如,我们可以利用如下事实来近似 π:6

/

π

2 is the probability

that two integers chosen at random will have no factors in common; that is,

that their greatest common divisor will be 1. To

obtain the approximation to π, we perform a large number of experiments.

In each experiment we choose two integers at random and perform a test to see

if their GCD is 1. The fraction of times that the test is passed

gives us our estimate of 6

π

2 是随机选取的两个整数互质的概率,即它们的最大公约数为 1 的概率。为了得到 π 的近似值,我们进行大量实验:每次实验随机选取两个整数,并测试它们的 GCD 是否为 1。测试通过的比例就给出了 6

/

π

2, and from this we obtain our

approximation to π.

π

2 的估计值,由此得到 π 的近似值。

The heart of our program is a procedure monte-carlo, which takes as

arguments the number of times to try an experiment, together with the

experiment, represented as a no-argument procedure that will return either true

or false each time it is run. Monte-carlo runs the experiment for the

designated number of trials and returns a number telling the fraction of the

trials in which the experiment was found to be true.

我们程序的核心是过程 monte-carlo,它接受两个参数:实验的尝试次数,以及实验本身——实验被表示为一个无参数的过程,每次运行时返回真或假。monte-carlo 将实验运行指定次数的试验,并返回一个数,表示试验中实验结果为真的比例。

Now let us try the same computation using rand-update directly rather

than rand, the way we would be forced to proceed if we did not use

assignment to model local state:

现在让我们尝试直接使用 rand-update 而非 rand 来完成同样的计算——这正是我们在不使用赋值来模拟局部状态时不得不采取的方式:

While the program is still simple, it betrays some painful breaches of

modularity. In our first version of the program, using rand, we can

express the Monte Carlo method directly as a general monte-carlo

procedure that takes as an argument an arbitrary experiment procedure.

In our second version of the program, with no local state for the random-number

generator, random-gcd-test must explicitly manipulate the random numbers

x1 and x2 and recycle x2 through the iterative loop as the

new input to rand-update. This explicit handling of the random numbers

intertwines the structure of accumulating test results with the fact that our

particular experiment uses two random numbers, whereas other Monte Carlo

experiments might use one random number or three. Even the top-level procedure

estimate-pi has to be concerned with supplying an initial random number.

The fact that the random-number generator’s insides are leaking out into other

parts of the program makes it difficult for us to isolate the Monte Carlo idea

so that it can be applied to other tasks. In the first version of the program,

assignment encapsulates the state of the random-number generator within the

rand procedure, so that the details of random-number generation remain

independent of the rest of the program.

虽然程序仍然简单,但它暴露了一些令人痛苦的模块化缺陷。在使用 rand 的第一个版本中,我们可以将蒙特卡罗方法直接表达为通用的 monte-carlo 过程,它接受一个任意的实验过程作为参数。在没有随机数生成器局部状态的第二个版本中,random-gcd-test 必须显式地操作随机数 x1 和 x2,并将 x2 作为 rand-update 的新输入在迭代循环中循环传递。这种对随机数的显式处理,将积累测试结果的结构与"我们的特定实验恰好用到两个随机数"这一事实缠绕在一起,而其他蒙特卡罗实验可能只用一个或三个随机数。甚至顶层过程 estimate-pi 也不得不关心提供初始随机数的事宜。随机数生成器的内部细节渗漏到程序的其他部分,使我们难以将蒙特卡罗的思想孤立出来,以便将其应用于其他任务。在第一个版本中,赋值将随机数生成器的状态封装在 rand 过程内部,使得随机数生成的细节与程序的其余部分保持独立。

The general phenomenon illustrated by the Monte Carlo example is this: From the

point of view of one part of a complex process, the other parts appear to

change with time. They have hidden time-varying local state. If we wish to

write computer programs whose structure reflects this decomposition, we make

computational objects (such as bank accounts and random-number generators)

whose behavior changes with time. We model state with local state variables,

and we model the changes of state with assignments to those variables.

蒙特卡罗示例所揭示的一般现象是:从一个复杂计算过程的某个部分来看,其他部分似乎随时间而变化——它们具有隐藏的、随时间变化的局部状态。如果我们希望所编写的计算机程序的结构能够反映这种分解方式,就需要构造行为随时间变化的计算对象(例如银行账户和随机数生成器)。我们用局部状态变量来模拟状态,用对这些变量的赋值来模拟状态的改变。

It is tempting to conclude this discussion by saying that, by introducing

assignment and the technique of hiding state in local variables, we are able to

structure systems in a more modular fashion than if all state had to be

manipulated explicitly, by passing additional parameters. Unfortunately, as we

shall see, the story is not so simple.

人们很容易就此得出结论:通过引入赋值以及将状态隐藏在局部变量中的技术,我们能够以比所有状态都必须通过传递额外参数来显式操作时更具模块化的方式来构建系统。然而,正如我们将看到的,事情并没有那么简单。

Racket #lang sicp
x₂ = (rand-update x₁)
x₃ = (rand-update x₂)
Racket #lang sicp
(define rand
 (let ((x random-init))
 (lambda () (set! x (rand-update x)) x)))
Racket #lang sicp
(define (estimate-pi trials)
 (sqrt (/ 6 (monte-carlo trials
 cesaro-test))))
(define (cesaro-test)
 (= (gcd (rand) (rand)) 1))

(define (monte-carlo trials experiment)
 (define (iter trials-remaining trials-passed)
 (cond ((= trials-remaining 0)
 (/ trials-passed trials))
 ((experiment)
 (iter (- trials-remaining 1)
 (+ trials-passed 1)))
 (else
 (iter (- trials-remaining 1)
 trials-passed))))
 (iter trials 0))
Racket #lang sicp
(define (estimate-pi trials)
 (sqrt (/ 6 (random-gcd-test trials
 random-init))))

(define (random-gcd-test trials initial-x)
 (define (iter trials-remaining
 trials-passed
 x)
 (let ((x1 (rand-update x)))
 (let ((x2 (rand-update x1)))
 (cond ((= trials-remaining 0)
 (/ trials-passed trials))
 ((= (gcd x1 x2) 1)
 (iter (- trials-remaining 1)
 (+ trials-passed 1)
 x2))
 (else
 (iter (- trials-remaining 1)
 trials-passed
 x2))))))
 (iter trials 0 initial-x))