# 3.5.1流是延迟列表

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

正如我们在 [2.2.3](32) 部分中所见，序列可以用作组合程序模块的标准接口。 我们为操纵序列（例如`map`，`filter`和`accumulate`）制定了功能强大的抽象，它们以简洁而优雅的方式捕获了各种操作。

不幸的是，如果我们将序列表示为列表，则在以计算所需的时间和空间为代价的情况下，这种优雅就以效率低下为代价。 当我们将序列的操作表示为列表的转换时，我们的程序必须在过程的每个步骤中构造和复制数据结构（可能很大）。

为了弄清楚为什么如此，让我们比较两个程序来计算一个间隔中所有素数的总和。 第一个程序是以标准的迭代方式编写的： [[1]](67#footnote-1)

```js
function sum_primes(a, b) {
    function iter(count, accum) {
        if (count > b) {
            return accum;
        } else {
            if (is_prime(count)) {
                return iter(count + 1, count + accum);
            } else {
                return iter(count + 1, accum);
            }
        }
    }
    return iter(a, 0);
}
```

第二个程序使用 [2.2.3](32) 节的序列操作执行相同的计算：

```js
function sum_primes(a, b) {
    return accumulate((x, y) => x + y,
                      0,
                      filter(is_prime, 
                             enumerate_interval(a, b)));
}
```

在进行计算时，第一程序仅需要存储正累积的和。 相反，在`enumerate_interval`构造了间隔中数字的完整列表之前，第二个程序中的过滤器无法进行任何测试。 筛选器生成另一个列表，该列表又在折叠为总和之前传递给`accumulate`。 第一个程序不需要这么大的中间存储，我们可以将其视为递增枚举间隔，将每个素数添加到生成的总和中。

如果我们使用序列范式通过评估表达式来计算介于10,000到1,000,000之间的第二个素数，则使用列表的效率低下将变得十分痛苦。

```js
head(tail(filter(is_prime,
                 enumerate_interval(10000, 1000000))));
```

如果给定足够的时间和空间，此表达式的确会找到第二个质数，但是计算开销非常大。 我们构造了一个将近一百万个整数的列表，通过测试每个元素的素性来过滤该列表，然后忽略几乎所有结果。 在更传统的编程风格中，我们将对枚举和过滤进行交织，并在达到第二个质数时停止。

流是一个聪明的主意，它允许人们使用序列操作而不会产生将序列作为列表进行操作的代价。 使用流，我们可以实现两全其美：我们可以将程序优雅地表述为序列操作，同时获得增量计算的效率。 基本思想是安排仅部分构造流，并将部分构造传递给使用该流的程序。 如果用户尝试访问尚未构造的流的一部分，则该流将自动构造其自身足够多的部分以生成所需的部分，从而保留了整个流存在的错觉。 换句话说，尽管我们将编写程序就像处理完整序列一样，但是我们将流实现设计为自动，透明地将流的构造与它的使用交织在一起。

 <split>流的最基本形式类似于列表。 空流是`null`，非空流是对，而该对的`head`是数据项。 但是，表示非空流的一对`tail`不是流，而是返回流的_无效函数。 函数返回的流，我们称_为流_的尾部。 如果我们有一个数据项`x`和一个流`s`，则可以通过评估`pair(x, () =&gt; s)`来构造一个头为`x`而尾为`s`的流。_

为了访问非空流的数据项，我们仅将`head`与列表一起使用。 为了访问流`s`的尾部，我们需要_应用_ `tail(s)`，即评估`(tail(s))()`。 为了方便起见，因此我们定义

```js
function stream_tail(stream) {
    return tail(stream)();
}
```

我们可以像制作和使用列表一样来制作和使用流，以表示按顺序排列的聚合数据。 特别是，我们可以从第2章构建列表操作的流类似物，例如`list_ref`，`map`和`for_each`： [[2]](67#footnote-2)

```js
function stream_ref(s, n) {
    return n === 0
           ? head(s)
           : stream_ref(stream_tail(s), n - 1);
}
function stream_map(f, s) {
    return is_null(s)
           ? null
           : pair(f(head(s)),
                  () => stream_map(f, stream_tail(s)));
}
function stream_for_each(fun, s) {
    if (is_null(s)) {
        return true;
    } else {
        fun(head(s));
        return stream_for_each(fun, stream_tail(s));
    }
}
```

函数`stream_for_each`对于查看流非常有用：

```js
function display_stream(s) {
    return stream_for_each(display, s);
}
```

表示流尾的函数在访问时使用`stream_tail`进行评估。 这种设计选择使我们想起了我们在 [2.1.2](26) 部分中对有理数的讨论，我们看到我们可以选择实现有理数，以便将分子和分母降到最低 这些术语可以在构建时或选择时执行。 这两个有理数实现会产生相同的数据抽象，但是选择会影响效率。 流和普通列表之间存在类似的关系。 作为数据抽象，流与列表相同。 区别在于评估元素的时间。 对于普通列表，`head` `tail`都在构造时进行评估。 对于流，`tail`在选择时进行评估。

流的尾部是&lt;quote&gt;包装在函数中的&lt;/quote&gt;。 它是_延迟表达_，&lt;quote&gt;承诺&lt;/quote&gt;会在将来的某个时间评估表达式exp。 相应地，`stream_tail`强制尾部履行其诺言。 它选择该对中的`tail`，并评估在那里发现的延迟表达以获得其余的流。

## 实况转播

要查看此数据结构的行为方式，让我们分析上面看到的&lt;quote&gt;令人发指的&lt;/quote&gt;素数计算，并根据流重新格式化：

```js
head(stream_tail(stream_filter(
                    is_prime,
                    stream_enumerate_interval(10000, 
                                              1000000))));
```

我们将看到它确实有效地工作。

我们首先使用参数10,000和1,000,000调用`stream_enumerate_interval`。 函数`stream_enumerate_interval`是`enumerate_interval`的流类似物（ [2.2.3](32) 部分）：

```js
function stream_enumerate_interval(low, high) {
    return low > high
           ? null
           : pair(low,
                  () => stream_enumerate_interval(low + 1, 
                                                  high)); 
}
```

and thus the result returned by `stream_enumerate_interval`, formed by the `pair`, is[[3]](67#footnote-3)

```js
pair(10000, () => stream_enumerate_interval(10001, 1000000));
```

也就是说，`stream_enumerate_interval`返回表示为一对的流，其`head`为10,000，并且如果需要，`tail`可以枚举更多间隔。 现在使用`filter`功能的流类似物对此流进行素数过滤（ [2.2.3](32) 部分）：

```js
function stream_filter(pred, s) {
    return is_null(s)
           ? null
           : pred(head(s))
             ? pair(head(s),
                    () => stream_filter(pred, 
                                        stream_tail(s)))
             : stream_filter(pred,
                             stream_tail(s));
}
```

函数`stream_filter`测试流的`head`（10,000）。 由于这不是素数，因此`stream_filter`会检查其输入流的尾部。 对`stream_tail`的调用会强制评估延迟的`stream_enumerate_interval`，现在返回

```js
pair(10001, () => stream_enumerate_interval(10002, 1000000));
```

函数`stream_filter`现在查看此流的`head` 10,001，发现它也不是素数，强制另一个`stream_tail`，依此类推，直到`stream_enumerate_interval`产生素数10,007，然后`stream_filter`， 根据其定义，返回

```js
pair(head(stream), stream_filter(pred, stream_tail(stream)));
```

which in this case is

```js
pair(10007,
     () => stream_filter(is_prime,
               pair(10008,
                    () => stream_enumerate_interval(10009, 
                                                    1000000))
	      )
    );
```

现在，此结果以原始表达式传递给`stream_tail`。 这将强制延迟`stream_filter`，这反过来又继续迫使延迟`stream_enumerate_interval`直到找到下一个素数10,009。 最后，在原始表达式中传递给`head`的结果是

```js
pair(10009,
     () => stream_filter(is_prime,
               pair(10010,
                    () => stream_enumerate_interval(10011, 
                                                    1000000))
              )
    );
```

函数`head`返回10,009，并且计算完成。 仅测试了找到第二个素数所必需的整数个素数，并且仅枚举了间隔，以馈入素数滤波器。

通常，我们可以将延迟评估视为&lt;quote&gt;需求驱动的&lt;/quote&gt;编程，由此，流过程中的每个阶段仅激活到足以满足下一个阶段的程度。 我们所做的是将计算中事件的实际顺序与函数的外观结构分离。 实际上，就像传统编程风格一样，当计算是增量执行时，我们编写函数就好像流一次全部存在&lt;quote&gt;。&lt;/quote&gt;

## 优化

当构造流对时，通过将这些表达式包装在函数中，我们延迟了它们的尾部表达式的求值。 通过应用功能，我们会在需要时强制进行评估。

此实现足以使流按所宣传的那样工作，但是我们可以包含一个重要的优化。 在许多应用程序中，我们最终迫使相同的延迟对象多次。 这可能导致涉及流的递归程序的严重低效率。 （请参阅练习 [3.57](68#ex_3.57)[3.27](60#ex_3.27) 中描述的功能。 实现此目的的一种方法是使用以下函数，该函数将（无参数的）一个函数作为参数并返回该函数的备注版本。 记忆功能第一次运行时，它将保存计算结果。 在随后的评估中，它仅返回结果。

```js
function memo(fun) {	    
    let already_run = false;
    let result = undefined;
    return () => {
                     if (!already_run) {
                         result = fun();
                         already_run = true;
                         return result;
                     } else {
                         return result;
                     }
	         };
}
```

每当构造流对时，我们都可以使用`memo`。 例如，代替

```js
function stream_map(f, s) {
    return is_null(s)
           ? null
           : pair(f(head(s)),
                  () => stream_map(f, stream_tail(s)));
}
```

we can define an optimized function `stream_map` as follows:[[4]](67#footnote-4)

```js
function stream_map_optimized(f, s) {
    return is_null(s)
           ? null
           : pair(f(head(s)),
                  memo( () => stream_map_optimized(
                                 f, stream_tail(s)) ));
}
```

<exercise>Define a function `stream_combine` that takes a binary function and two streams as arguments and returns a stream whose elements are the results of applying the function pairwise to the corresponding elements of the argument streams.

```js
function stream_combine(f, s1, s2) {
    ...
}
```

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_67_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>In order to take a closer look at delayed evaluation, we will use the following function, which simply returns its argument after printing it:

```js
function show(x) {
    display(x);
    return x;
}
```

What does the interpreter print in response to evaluating each expression in the following sequence?[[5]](67#footnote-5)

```js
let x = stream_map(show, stream_enumerate_interval(0, 10));
stream_ref(x, 5);
stream_ref(x, 7);
```

<split>What does the evaluator print if `stream_map_optimized` is used instead of `stream_map`?

```js
let x = stream_map_optimized(
            show, 
            stream_enumerate_interval(0, 10));
stream_ref(x, 5);
stream_ref(x, 7);
```</split> <button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_67_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>Consider the sequence of expressions

```js
let sum = 0;

function accum(x) {
    sum = x + sum;
    return sum;
}

const seq = stream_map(
                accum, 
                stream_enumerate_interval(1, 20));
const y = stream_filter(is_even, seq);

const z = stream_filter(x => x % 5 === 0, seq);

stream_ref(y, 7);

display_stream(z);
```

What is the value of `sum` after each of the above expressions is evaluated? What is the printed response to evaluating the `stream_ref` and `display_stream` expressions? Would these responses differ if we had applied the function `memo` on every tail of every constructed stream pair, as suggested in the optimization above? Explain.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_67_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></split>

* * *

[[1]](67#footnote-link-1) Assume that we have a predicate `is_prime` (e.g., as in section <ref name="sec:primality">[1.2.6](17)</ref>) that tests for primality.

[[2]](67#footnote-link-2) This should bother you. The fact that we are defining such similar functions for streams and lists indicates that we are missing some underlying abstraction. Unfortunately, in order to exploit this abstraction, we will need to exert finer control over the process of evaluation than we can at present. We will discuss this point further at the end of section <ref name="sec:streams-and-delayed-evaluation">[3.5.4](70)</ref>. In section <ref name="sec:lazy-evaluation">[4.2](81)</ref>, we'll develop a framework that unifies lists and streams.

[[3]](67#footnote-link-3) The numbers shown here do not really appear in the delayed expression. What actually appears is the original expression, in an environment in which the variables are bound to the appropriate numbers. For example, `low + 1` with `low` bound to 10,000 actually appears where `10001` is shown.

[[4]](67#footnote-link-4) There are many possible implementations of streams other than the one described in this section. Delayed evaluation, which is the key to making streams practical, was inherent in Algol 60's _call-by-name_ parameter-passing method. The use of this mechanism to implement streams was first described by Landin (1965). Delayed evaluation for streams was introduced into Lisp by Friedman and Wise (1976). In their implementation, `cons` always delays evaluating its arguments, so that lists automatically behave as streams. The memoizing optimization is also known as _call-by-need_. The Algol community would refer to our original delayed objects as _call-by-name thunks_ and to the optimized versions as _call-by-need thunks_.

[[5]](67#footnote-link-5) Exercises such as <ref name="ex:delayed1">[3.51](67#ex_3.51)</ref> and <ref name="ex:delayed2">[3.52](67#ex_3.52)</ref> are valuable for testing our understanding of how delayed evaluation works. On the other hand, intermixing delayed evaluation with printing—and, even worse, with assignment—is extremely confusing, and instructors of courses on computer languages have traditionally tormented their students with examination questions such as the ones in this section. Needless to say, writing programs that depend on such subtleties is odious programming style. Part of the power of stream processing is that it lets us ignore the order in which events actually happen in our programs. Unfortunately, this is precisely what we cannot afford to do in the presence of assignment, which forces us to be concerned with time and change.

