3.5.3 探索用流来编程的模式

3.5.3 探索流的模式
带有延迟解释的流能成为一个强有力的建模工具,提供了局部变量和赋值的好处中的许多的部分。
进而它们能避免一些理论性的混乱,即把赋值语句引入到一种编程语言中。

流方法能成为富于启发性的, 因为它允许我们以不同的模块边界来构建系统,而不是围绕着
对状态变量的赋值来组织起系统。
例如,我们能认为一个整体上的时间系列(或者是信号)作为一个兴趣的关注点,而不是在独立的时刻
的一个状态变量的值。这使得它能够在 组合和比较从不同的时刻来的状态的组件的方面有了便利性。

* 公式化迭代成为流处理
在1.2.1部分中,我们介绍了迭代的过程,它使用了更新的状态变量。我们现在知道我们能表示
状态为一个无时间性的数据的流而不是需要被更新的变量的集合。让我们采用这个新的视角,
改写1.1.7部分中的求平方根的程序。回忆一下为了生成越来越好的对于平方根的猜测值的序列,
通过一遍又遍地应用改善猜测值的程序:

(define (sqrt-improve guess x)
(average guess (/  x guess)))

在我们的原始的求平方根的程序中,我们能把这些猜测值作为一个状态变量的连续的值。
代替为我们能生成一个猜测值的无限流,开始于一个初始化的猜测值1:

(define (sqrt-stream x)
   (define guesses
               (cons-stream 1.0
                                     (stream-map (lambda (guess)
                                                                         (sqrt-improve guess x))
                                                           guess)))
guesses)

1
1.5
1.416666666
1.414215686
1.4142135623
......
为了得到越来越好的猜测值,我们能生成流的越来越多的项。如果我们愿意,
我们能写一个程序,来保证生成的项,它的答案值足够得好。(见练习3.64)

另一个迭代的过程是我们能以相同的方式处理的是为了生成合适的圆周率值。
基于我们在1.3.1部分中看到的多项式:

pi/4=1-1/3+1/5-1/7+......

我们首先生成多项式的加法的流(奇整数的倒数,带有负号)
然后,我们得到了和的流的越来越多的项(使用练习3.55中的程序partial-sums)
结果再乘以4。

(define ()
     (cons-stream (/ 1.0 n)
                          (stream-map  -  (pi-summands (+ n 2))))
)
(define pi-stream
     (scale-stream (partial-sums  (pi-summands 1))
4))

(display-stream pi-stream)
4.
2.666666
3.466666
2.895238
3.339682
2.976046
3.283738
3.011707
....
尽管合适的值的收敛是相当得慢,这给我们一个圆周率的越来越好的
合适的值的流。序列的前八项,圆周率的值介于3.284与3.017之间。

因而,我们的状态方法的流的使用与更新状态变量的没有多大的不同。但是
流给我们一个做一些有趣的事的机会。例如,我们能转换一个流,
用一个序列的累加器,这个工具把解的序列转换成一个新的序列,
从原始值收敛到相同的结果值,只是更快了些。

这样的累加器,归功于18世纪的瑞士数学家 莱昂哈德*欧拉,在有
交互式多项式(有的项是负值的多项式)的部分和的序列上很有效。
在欧拉的技术,如果Sn是原始的和的序列的第n项,那么,累加的序列有如下的项:

S(n+1) - (S(n+1)-Sn)^2/(S(n-1)-2*S+S(n+1))

因此,如果原始的序列是被表示为一个值的流,转换的序列如下:

(define (euler-transform s)
   (let  ((s0 (stream-ref  s  0))          ;S(n-1)   
           (s1 (stream-ref  s  1))          ;Sn
           (s2 (stream-ref  s  2)))         ;S(n+1)
         (cons-stream  (- s2 (/  (square  (-   s2  s1))
                                             (+  s0  (*  -2   s1)   s2)))   
                                (euler-transform  (stream-cdr  s))))
)

我们能演示欧拉累加用求圆周率值的序列:

(display-stream (euler-transform pi-stream))
3.16666666666
3.1333333333333
3.14523809
3.1396825
3.1424128427
3.1408813408
3.1420718
3.141254823
.....

更好的是,我们能累加已经累加过的序列,递归地累加它等等。
命名上,我们能创建一个流的流(结构上我们叫它为表体),它的每个流
都是如下的处理:

(define (make-tableau transform s)
    (cons-stream   s 
                           (make-tableau transform 
                                                   (transform s)))
)

表体有如下的形式:
S00  S01 S02 S03 S04    ....
        S10  S11 S12 S13   ....
                S20  S21 S22  ....
                     ....

最后,我们通过取表体的每一行的第一个项形成了一个序列:

(define (accelerated-sequence transform s)
    (stream-map stream-car
                         (make-tableau transform s))
)

我们能演示这种超级累加的圆周率的序列:
4.
3.166666666666
3.142105263157
3.141599357319
3.141592714033
3.141592653975
3.141592653591
3.141592653589778
....

结果是让人印象深刻的,序列的前八项,列举的值,精度达到十进制的14位小数的正确值。
如果我们仅使用原始的圆周率的序列,我们将需要计算的项的数量是10^13的量级上才能
到达同样的精度。 我们能在没有使用流的情况下,实现这种加速的技术。但是流的规范化
是特别优雅的与一致性的,因为状态的整个序列对于我们而言是可用的,正如一个能被
一系列的操作所控制的一种数据结构。


练习3.63
罗斯问为什么sqrt-stream程序没有写成如下的更自然的方式,没有局部变量
guesses:

(define (sqrt-stream x)
               (cons-stream 1.0
                                     (stream-map (lambda (guess)
                                                                         (sqrt-improve guess x))
                                                            (sqrt-stream x))))
阿丽莎回答是程序的这个版本没有效率,因为它执行了冗余的计算,解释阿丽莎的答案。
如果我们的delay的实现是仅使用了(lambda () <exp>)而没有使用3.5.1部分中提供的memo-proc
来优化的话,这两个版本的程序仍然有效率的差异吗?

练习3.64
写一个程序,stream-limit,它以一个流和一个数为参数,这个数是误差,它应该检查流
直到发现连续的两个数的差值的绝对值小于误差,并且返回这两个元素的第二个元素。使用这个程序
我们能计算平方根,满足指定的误差:

(define (sqrt x tolerance)
    (stream-limit (sqrt-stream x)   tolerance)
)

练习3.65
使用序列
ln2=1-1/2+1/3-1/4+.......
来计算2的自然对数的解的三个序列,用我们计算圆周率的相同的方式。
这些序列的收敛速度有多快?

*数对的无限流
在2.2.3部分中,我们看到了序列模式如何处理嵌套的循环,就好像过程
是定义在数对的序列上似的。如果我们泛化了这个技术到无限流上,那么
我们能写程序就不容易表示成循环了,因为循环必须要有一个范围包括一个无限的集合。

例如,假定我们要泛化一个2.2.3部分中的prime-sum-pairs程序,为了生成所有的
两个数的数对,其中第一个数小于等于第二个数,两个数的和是素数。如果int-pairs
是所有的两个数的数对的序列,并且第一个数小于等于第二个数,那么我们需要的流是简单的:

(stream-filter  (lambda (pair)   
                                   (prime? (+ (car pair)   (cadr  pair))))
     int-pairs
)

我们的问题,是生成流int-pairs.更宽泛地说,假定我们有两个流,
S=(Si)和 T=(Tj),并且可以想象的是,无限的矩形的数组是:

(S0  T0)  (S0 T1)  (S0  T2) .....
(S1  T0)  (S1 T1)  (S1  T2) .....
(S2  T0)  (S2 T1)  (S2  T2) .....
...

我们的期望是生成一个流包括所有的如下的数对,它位于对角线的上方。例如:
(S0  T0)  (S0 T1)  (S0  T2) .....
              (S1 T1)  (S1  T2) .....
                           (S2  T2) .....
                                        ......
(如果我们以S和T作为整数流,那么这上面的将是我们的期望的流 int-pairs)

调用数对的通用的流(pairs S T),让它成为三个部分的组合:(S0 T0)的数对,
在第一个行中的其它的部分,其它的数对:

(S0  T0) | (S0 T1)  (S0  T2) .....
________ |______________________
             | (S1 T1)  (S1  T2) .....
             |              (S2  T2) .....
             |                           ......

注意的是在这个分解的第三个部分(不在第一行的数对)
是递归性地由(stream-cdr S) 和(stream-cdr T)组成的。
也要注意第二部分,是第一行的其余部分是:

(stream-map (lambda (x)  (list (stream-car s) x))
                      (stream-cdr t))

因此,我们能形成如下的数对的流:

(define (pairs s t)
    (cons-stream (list (stream-car s) (stream-car t))
        (<combine-in-some-way>        
            (stream-map (lambda (x)  (list (stream-car s) x))
                      (stream-cdr t))
                     (pairs (stream-cdr s) (stream-cdr t)))
        )
)

为了完成这个程序,我们必须选择某种方式来组合两个内部的流。
使用这个流的思想与2.2.1部分的append程序类似:

(define (stream-append s1 s2)
   (if (stream-null? s1)
      s2
      (cons-stream (stream-car s1)
                           (stream-append (stream-cdr s1) s2))
   )
)

对于无限流,这是不合适的,然而,因为它在集成第二个流之前它要取第一个流的所有的元素。
特别是,如果我们要试图生成正整数的所有的数对之时:

(pairs integers integers)

我们的流的结果将首先试图运行,以第一个整数为1的所有的数对,因此将没有产生第一个整数的
其它的值的数对。

为了处理无限流,我们需要明确一个组合的顺序,来确保在我们让程序运行足够长的时间后,
任何一个元素将最终能到达。一个优雅的方式来完成这个任务的是使用下面的程序interleave.

(define (interleave s1 s2)
   (if (stream-null? s1)
      s2
      (cons-stream (stream-car s1)
                           (interleave s2 (stream-cdr s1)))
   )
)

因为interleave从两个流中,交替地取元素,即使第一个流是无限的,
第二个流中的元素将最终能找到它的方式进入交替的流中。

我们能因此生成需要的数对的流如下:

(define (pairs s t)
    (cons-stream (list (stream-car s) (stream-car t))
        (interleave       
            (stream-map (lambda (x)  (list (stream-car s) x))
                      (stream-cdr t))
                     (pairs (stream-cdr s) (stream-cdr t)))
        )
)


练习3.66
检查流(pairs integers integers). 你能做一个通用的解释,关于数对进入流的复杂度的
增长数量级吗? 例如,在(pair 1 100)或者(pair 99 100)或者(pair 100 100)有
多少个数对吗? (如果你能给出一个精确的数学公式,就更好了。如果你发现自己有一点不清楚
给出数量值也是可以的。) 

练习3.67
修改pairs程序,为了(pairs integers integers)将生成数对的所有的部分放入流中。没有两个数的大小限制了。
提示:你能需要一个附加的流来混合操作。

练习3.68
罗斯认为用三部分构建一个数对的流是非必要的复杂。代替把数对(s0,T0)从第一行中的数对分离出来,他
写出了以第一个整行,如下:

(define (pairs s t)
           (interleave       
                   (stream-map (lambda (x)  (list (stream-car s) x))
                     t)
                     (pairs (stream-cdr s) (stream-cdr t))
           )   
)

这能有效吗?如果我们用罗斯的定义来解释(pairs integers integers),考虑一下,能发生什么情况?

练习3.69
写一个程序triples,它接受三个无限流为参数,生成三元组(Si Tj  Uk)的流,满足i<=j<=k.
使用程序triples生成正整数的所有的勾股定理的三元组的流。就是 i*i+j*j=k*k.

练习3.70
用一些有特定的有用的顺序的数对来生成流,就是更好的了,而不是
用交替过程生成的一般的顺序。如果我们能定义一个方式来表达一个
数对小于另一个数对,那么我们能使用一个技术与练习3.56中的merge程序
相似的方法。实现这个任务的一个方式是定义一个权重的函数W(i,j),如果 W(i1,j1)< W(i2,j2),
那么就认为是(i1,j1)<(i2,j2)。写一个程序merge-weighted,它是与merge很像的,除了它
需要一个额外的参数是权重,这是一个程序,来计算一个数对的权重,并且被用来确定出现在合并的流中的
元素的前后的顺序。使用这个程序,泛化pairs 为一个新的程序weighted-pairs,它以两个流,加上一个
计算权重函数的程序为参数,生成数对的流,并且根据权重值进行排序。使用你的程序生成如下的流:

a.正整数(i,j)并且i<=j,的所有的数对的流,以i,j的和排序。
b.正整数(i,j)并且i<=j,的所有的数对的流,ij都不能被2,3,5整除,数对以2*+3*j+5*i*j的大小排序。

练习3.71
数被表现为两个数的立方和,如果有超过一种方式的,叫做拉马努金数,以纪念数学家拉马努金。
对于计算这样的数的问题,数对的有序流提供了一个优美的方案。为了找到一个数,能被写成两个
数的立方和,并且是以两种不同的方式,我们仅需要生成整数i,j的数对的流,权重是ij的立方和。(见练习3.70)
然后搜索流中具有相同权重的连续的两个数对。写一个程序,生成拉马努金数。第一个拉马努金数是1729,
接下来的五个拉马努金数是什么?

练习3.72
与练习3.71的内容相似,生成一个流,它的所有的数能以三种方式写成两个数的平方和(并且显示如何写成的)

*流作为信号
与信号处理系统中的信号相似,通过描述流为计算,我们能开始了流的讨论。在事实上,
我们能使用流来对信号处理系统进行直接方式的建模,把一个连续的时间间隔的信号的值表示为一个流中的元素。
例如,我们能实现一个积分或者是求和,对一个输入流 x=(xi),一个初始值C,一个小的增量dt,累加求和

Si=C+ 求和符号 j从1到i xj *dt

并且返回S=(Si)的值 的流。如下的integral程序是3.5.2部分中的整数的流的隐式的风格的定义的
一个翻版。

(define (integral integrand initial-value dt)
   (define int
             (cons-stream inital-value
                                   (add-streams (scale-stream integrand dt)
                                                        int)))
int)

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/82813069