流
对现实世界事物建模时,由于事物中存在随时间变化的状态,目前只能通过赋值和可变对象实现。虽然可以解决建模的问题,但由于赋值的引入带来了许多棘手的问题,于是我们亟需一种既能完成建模需求,又不使用赋值操作(避免赋值操作带来的问题)的方法。
这种新的解决方案为数据结构——流 (streams)。从数据抽象的角度出发,流与列表是一致的,但列表在数据规模增大时会极大消耗计算资源。因为在列表的流式操作中(filter
、map
、accumulate
等操作)需要不断拷贝和生成新的全量列表,在最终只需要其中少量数据参与计算的情况下,这种代价是巨大的。所以流与列表的不同之处在于它能够延迟计算(或者说按需计算),当元素尚未被需要时流不会主动对其进行计算。
流的操作
-
(cons-stream x y)
:流的构造器,与表达式(cons x (delay y))
等价 -
(stream-car <stream>)
:流的选择器,与表达式(car <stream>)
等价 -
(stream-cdr <stream>)
:流的选择器,与表达式(force (cdr <stream>))
等价 -
the-empty-stream
: 一个特殊对象,对其应用cons-stream
不会返回任何结果,不过可以通过stream-null?
判断
流的实现将基于特殊形式 delay
。(delay <exp>)
并不会计算表达式 <exp>
,而是返回 延迟对象(delay object),可以将其视作能够在未来时间进行 <exp>
运算的承诺。经常伴随 delay
出现的程式 force
能够接收延迟对象作为参数并执行其中的表达式,实际上是要求 delay
实现它的承诺。
delay 与 force
delay
需要包装一个程式,并在之后按需执行它,其实可以简单地将表达式处理成程式体实现。所以 (delay <exp>)
相当于 (lambda () <exp>)
的语法糖。force
能够调用由 delay
产生的程式,所以可以按如下方式实现 force
。
(define (force delayed-object) (delayed-object))
在许多应用中,需要对同一个延迟对象进行多次调用,这将导致流中的递归程序效率降低。所以需要使延迟对象只在第一次执行时被构建,然后再将计算结果进行存储。后续对同一延迟对象的强制执行不再重复计算而是返回存储的结果。缓存操作实现如下。
(define (memo-proc proc)
(let ((already-run? false) (result false))
(lambda ()
(if (not already-run?)
(begin (set! result (proc))
(set! already-run? true)
result)
result))))
delay
的实现改写为 (memo-proc (lambda () <exp>))
。
无限流
基本的流,它看起来拥有完整的元素,但实际上只计算当前访问时需要的元素。在这种情况下,即使序列很长也可以通过流进行高效的计算。更引人注目的是还可以通过流表示无限长的序列。按如下方式可以实现一个整数的无限流。
(define (integers-starting-from n)
(cons-stream n (integers-starting-from (+ n 1))))
(define integers (integers-starting-from 1))
具体使用如下。
(stream-ref integers 100)
100
其中的 stream-ref
功能与 list-ref
类似,能够读取流中的第 n 个元素。
隐式流
除了上述的方式实现无限流外,还可以通过自引用的方式形成循环回调,实现无限流。下列程式将实现一个斐波那契数列的无限流。
(define fibs
(cons-stream
0
(cons-stream 1 (add-streams (stream-cdr fibs) fibs))))
其中 add-streams
能够计算两个流逐个元素的和,并形成一个新流。
流的弊端
虽然流在建模上避免了赋值操作带来的问题,完全继承了函数式编程的优势,但它不够直观,使程式的实现和理解变得复杂。而且即使流没有通过赋值和局部状态变量为程序带来现实世界的时间问题,但时间问题依然存在,不过它被转移到了整个系统上而已,特别是在独立实体之间的建模上时间问题将彻底浮现。它与可变对象在建模上都有自身的优势,但也都不完美,完美的方法依然尚未出现。
网友评论