《计算机构造与解释》读书笔记(5)

  • Post author:
  • Post category:其他






1. 写在最前面

很佩服那种无论过什么节都没有过节综合征的小伙伴,在过了 7 天假期,然后又连续上班 7 天的魔鬼模式下,最近很容易力不从心。

先默默的整理节前没有写好的笔记,调整一下状态吧!



2. 流

是否存在另外一种方法,避免使用「赋值」。

在本小节中,将基于一种称为流的数据结构,流可能缓和状态模拟中的复杂性。流的使用可以模拟一些包含状态的系统,却不需要利用赋值或者变动数据。



2.1 流作为延时的表

以计算一个区间里的素数之和为例,对比一下两个程序:

  • 迭代风格的程序

    (define (sum-primes a b)
       (define (iter count accum)
         (cond (( > count b) accum)
               ((prime? count) (iter (+ count 1) (+ count accum)))
               (else (iter (+ count 1) accum))))
       (iter a 0))
    
  • 递归风格的程序

    (define (sum-primes a )
      (accumulate + 
                  0
                  (filter prime? (enumerate-interval a b))))
    

对比以上两种方法,通过序列方式去计算从 10000 到 1000000 的区间,第二种递归方式,效率明显太低了。

注: 流是一种非常巧妙的想法,可以利用各种序列操作,但又不会带来将序列作为表去操作而引起的代价。

作为一种数据抽象,流与表完全一样。它们的不同点在于元素的求值时间。对于常规的表,其 car 和 cdr 都是在构造时求值;而对于流,其 cdr 则是在选取的时候才去求值。

注:流的实现将基于一种称为 delay 的特殊形式,对于(delay ) 的求值将不对表达式 求值,而是返回一个延时对象的对象,它可以看作是对在未来的某个时间求值 的允诺。

​ 与 delay 一起的还有一个称为 force 的过程,它以一个延时对象为参数,执行相应的求值工作。



2.1.1 流实现的行为方式

一般而言,可以将延迟求值看作一种「由需要驱动」的程序设计,其中流处理的每个阶段都仅仅活动到足够满足下一个阶段需要的程度。



2.1.2 delay 和 force 的实现

delay 必须包装一个表达式,使它可以在以后根据需要求值。

force 是简单地调用由 delay 产生的过程

注:构造 delay 时存在优化的手段,就是将构造 delay 的值进行缓存,再下次调用出现相同的调用时,直接返回值。



2.2 无穷流

可以用流去表示无穷长的序列,例如,考虑下面关于正整数的流的定义:

(define (integers-starting-from n)
  (cons-stream n (integers-starting-from (+ n 1))))
(define integers (integers-starting-from 1))

注:这是一个无穷长的流,但是在任何给定时刻,都能能检查到它的有穷部分。



2.2.1 隐式地定义流

描述流的另一种方式是利用延时求值隐式地定义流。举个例子,下面表达式将 ones 定义为 1 的无穷流:


(define ones (cons-stream 1 ones))

这种方式定义就像是在定义一个递归过程:这里的 ones 是一个序对,它的 car 是 1,而 cdr 是求值 ones 的一个允诺。对于其 cdr 的求值又是一个 1 和 求值 ones 的一个允诺,并这样继续下去。



2.3 流计算模式的使用

带有延迟求值的流可能成为一种功能强大的模拟工具,能提供局部状态和赋值的许多效益。

注:这种机制还能避免将赋值引入程序设计语言所带来的一些理论困难。流方法极富有启发性,因为借助于它去构造系统时,所用的模块化划分方式可以与采用赋值、围绕着状态变量组织系统的方式不同。



2.3.1 系统地将迭代操作方式表示为流过程

  • 状态可以表示为值的「没有时间的」流,而不是一组不断更新的变量。
  • 可以构造出一个流的流(称为表列的结构),其中的每个流都是前一个流的变换的结果。



2.3.2 序对的无穷流

思考 prime-sum-pairs 过程,生成所有整数序对(i,j)的流,其中有 i <= j 且 i + j 是素数。

为了处理无穷的流,需要设计另一种组合序列,以保证只要这个程序运行的时间足够长,那么最终就能得到流中的每一个元素。做到这一点的方式是采用下面的 interleave 过程:

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

interleave 交替地从两个流中去元素,这样,即使第一个流是无穷的,第二个流里每个元素最终都能在这样交错得到的流里有自己的位置。



2.3.3 将流作为信号

可以采用流,以一种非常直接的方式为信号处理系统建模,用流的元素表示一个信号在顺序的一系列时间间隔上的值。



2.4 流和延时求值

定义反馈循环的能力依赖于 delay

注:如果没有 delay ,要求对每个信号处理部件的输入都能在产生输出之前完成求值。



2.4.1 规范求值序

可以采用一种求值模型,其中所有过程参数都自动延时,只有在实际需要它们的时候才强迫参数求值。即

规范序求值

在使用 integral (积分)的过程时,我们构造出了两类过程:

  • 常规过程
  • 要求延迟参数的过程

注:如果创建了不同种类的过程,就将迫使去创建不同种类的高阶过程



2.5 函数式程序的模块化和对象的模块化

  • 引进赋值可以增强系统的模块化,将一个大系统的状态中的某些部分封装在局部变量里。
  • 流模型不使用赋值,但是可以提供跟赋值同样的模块化能力。



2.5.1 时间的函数式程序设计观点

引进赋值和变动对象,就是为了提供一种机制,以便能模块化地构造出程序,去模拟具有状态的系统。

注:流为模拟具有内部状态的对象提供了另一种方式,可以用一个流去模拟一个变化的量,例如某个对象的内部状态,用流表示其顺序状态的时间史。

考虑重新设计一个「取款处理器」的实现:

  • 赋值版本

    (define (make-simplified-withdraw balance)
      (lambda (amount)
        (set! balance (- balance amount))
        balance))
    

    调用 make-simplified-withdraw 将生成出具有局部变量 balance 的对象,其值将在对这个对象的一系列调用中逐步减少。这些对象以 amount 为参数,返回一个新的余额值。

  • 流版本,以越值和一个提款流作为参数,生成账户中顺序余额的流

    (define  (stream-withdraw balance amount-stream)
      (cons-stream
       balance
        (stream-withdraw (- balance (stream-car amount-strem))
                         (stream-cdr amount-stream))))
    
    stream-withdraw 实现具有良好定义的数学函数,其输出完全由输入确定。该函数其行为根本不会变化,用户看到的却是与一个改变状态的系统交互。
    

注:解释上述原因很好的方式是,用户方时态的存在,为这个系统赋予了状态特性。如果用户从自己交互问题上后退一步,以余额流的方式思考问题,而不是去看个别的交易,这个系统看上去就是无状态的了。



3. 碎碎念

断断续续的啃完了这本书的前三章,深刻的意识到,「观念的改变」从不在一朝一夕,而是需要不断的潜移默化影响自己。

再定个小目标吧,希望这个月能够把 go 新语法补充一下,实在木有时间的话,那也得学习一下泛型吖!

  • 好为人师是一种炫耀,潜意识告诉别人你比他强,这种姿态只要一出来,别人的反感就会生出来。
  • 跟蠢人交流你会有一个很明显的感觉:你说的每一句话,他都想要反驳。
  • 努力了却没看到回报 那就是你现在的努力只是在弥补你之前的懒惰



版权声明:本文为phantom_111原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。