3.5.5 函数式程序的模块性与对象的模块性

3.5.5 函数式程序的模块性与对象的模块性
正如我们在3.1.2部分中看到的,介绍赋值语句的主要的好处之一,我们能通过封装或者是隐藏
一个大规模的有局部变量的系统的状态,增加了我们的系统的模块性。流模型能提供一个等价
的模块性却没有使用赋值语句。正如演示的那样,我们能重实现蒙特卡罗的方法计算圆周率,
看我们在3.1.2部分,从一个流处理的视角。

关键的模块化问题是我们期望从使用随机数的程序中隐藏一个随机数生成器的内部状态。
我们开始一个程序rand-update,它的连续值满足了随机数的供应,并且使用它来生成
一个随机数的生成器。

(define rand (let ((x random-init))
    (lambda () 
                  (set! x (rand-update x))
                  x)
))

在流公式化这是没有随机数生成器perse,正如以对rand-update的连续调用
,实现随机数的一个流生成。

(define random-numbers
    (cons-stream  random-init 
                           (stream-map  rand-update random-numbers))
)

我们使用这来组装执行在random-numbers流中的连续的数对的基础上,
塞萨罗实验的结果的流:

(define cesaro-stream
            (map-successive-pairs (lambda (r1 r2) (= (gcd r1 r2)  1))
                                             random-numbers))

(define   (map-successive-pairs  f s)
    (cons-stream  (f  (stream-car s)  (stream-car (stream-cdr  s)))
           (map-successive-pairs  f (stream-cdr  (stream-cdr  s))))
)

cesaro-stream现在被喂给了monte-carlo程序,它生成了概率的估值的流。
这结果然后被转换成了圆周率的估值的流。程序的这个版本不需要一个参数
告诉有多少个试验值要执行。通过查找 圆周率的流的更多的项,能得到越来越好的
圆周率的估值:

(define (monte-carlo experiment-stream  passed failed)
      (define  (next passed failed)
            (cons-stream  (/  passed (+ passed failed))
                                   (monte-carlo  (stream-cdr  experiment-stream)  passed failed))
      )
     (if (stream-car experiment-stream) 
           (next (+ passed 1)  failed) 
           (next  passed  (+ failed 1)))
)

(define pi
       (stream-map  (lambda (p) (sqrt  (/ 6 pi)))
                              (monte-carlo  cesaro cesaro-stream 0 0)
))

在这个方法中,有相当的模块性,因为我们仍然能够公式化一个通用的monte-carlo
程序,能处理随意的实验。然而没有赋值和局部变量。

练习3.81
练习3.6中讨论了泛化随机数生成器,来允许一个重置随机数的序列,为了生成
随机数的可重复的序列。生成同一个生成器的随机数的序列,为了生成一个新的
随机数,操作一个请求的输入流,或者重置一个指定的值的序列,来生成随机数
的目标流。在你的解决方案中不要使用赋值语句。

练习3.82
使用流这个术语,重做练习3.5中的蒙特卡罗积分。estimate-integral的流的版本,
没有一个参数来告诉要生成多少个执行的值。相反的是,它将在连续的更多的值
的基础上,生成一个估值的流。

*时间的一个函数式编程的视角
现在让我们返回本章开始时提到的状态与对象的问题,在一种新的视角下审视它们。
我们介绍了赋值语句与可交互式的对象,来提供一种机制,这种机制实现了对有状态
的系统进行建模的程序的模块化组装。我们组装了有局部状态变量的计算对象,并且
使用赋值语句来改变这个变量。我们建模了世界中对象的这种临时性的行为,通过相对应
的计算对象的临时性的行为。

现在我们已经看到了流提供了另一种可行的方式来建模有局部状态的对象。我们能建模
一个可变的数量,例如一些对象的局部状态,使用流,表示连续的状态的时间历史。
在本质上,我们显式的表示了时间,使用流,于是在我们的模似的世界中,我们从间隔发生
的事件的序列中,解出了时间。的确,因为delay的存在,在模型中模拟的时间与解释中的事件的顺序
可能没有什么关系了。

为了比较建模的这两个方法,让我们重新审视一个取款处理过程的实现。在3.1.3部分中,
我们实现了一个简单的版本。

(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-stream))
                                             (stream-cdr amount-stream)))
)

stream-withdraw实现了一个定义良好的数学函数,它的输出完成取决于输入。假定
然而,输入amount-stream是用户输入的连续的值的流,余额的结果流是被显示的。
那么,从用户的角度来看,流处理过程与对象创建过程有完全一致的行为。然而,
流的版本,没有赋值语句,没有局部变量,因此在3.1.3部分中我们没有遇到任何的理论性
困难,只是系统有状态!

这是真得有名的,即使stream-withdraw实现了一个定义良好的数学函数,它的行为没有
改变,这里用户的视角上,与系统的交互之一已经修改了一个状态。解决这个幻觉的一个
方式是意识到是用户的潜意识中在系统上强加了一个状态。如果用户能从交互中跳出来,
用余额的流的方式想问题,而不是独立的事务,系统就变得没有状态了。

从一个复杂的过程的一部分来看,其它的部分似乎随着时间而改变。它们隐藏了随时间而变的
局部变量。如果我们期望写程序,建模我们的世界中的自然的部分为我们的计算机中的结构,
我们创建了非函数式的计算对象,它们必须而时间而变。我们建模状态为局部变量,我们建模
状态的改变为对那些变量的赋值语句。通过这么做,我们对世界中的时间做成了计算机中执行的
时间,因此在我们的计算机中我们得到了对象。

以对象建模是强有力的,并且是直觉的,主要是因为这匹配了我们的世界中的交互的视角。
然而,正如我们已经看到的在本章中不断地重复的那样,这些模型带来了限制事件的顺序和
同步多个过程的问题。避免这些问题的可行的方法是使用函数式的编程语言的开发,它不提供
赋值语句和可交互的数据。在这种语言中,所有的程序实现为有参数的定义良好的数学函数,
它的行为不变。在处理并发式的系统时,函数式的方法是极其有吸引力的。

另一个方面,如果我们详细地观察,我们能看到时间有关的问题也进入了函数式的模型。
当我们要设计一个交互式的系统时,一个特定的问题域出现了,特别是在独立的实体之间建模
交互行为。例如,考虑一个银行系统的更多的实现,允许联合账户。在一个传统的系统中,使用
赋值和对象,我们对彼得和保罗共享一个账户,并且都发出事务性的请求的事实进行建模。
从流 的视角看,没有对象存在,我们已经显示 了一个银行账户能被建模成一个过程,这个过程
操作一个事务请求的输入流,生成一个响应的输出流。相应的是,我们对彼得和保罗共享一个账户
,并且合并了彼得的事务性的请求的流和保罗的请求的流并且把结果输出到银行账户的流的过程的
事实进行建模。

这个规范的困难在于合并的概念。合并两个流并不是简单地取交替地从两个请求中取元素。
假定,保罗很少读取账户。在彼得发出第二个事务之前,我们几乎不能强制彼得等待保罗读取账户。
然而,这样的合并被实现了,它必须以某种方式交替两个事务流,这种方式被约束为彼得和保罗接受
的实时,以智能的方式,如果彼得和保罗开会,他们能同意特定的事务在开会前执行,其它的事务
在开会后执行。这是与在3.4.1部分中我们不得不处理约束有相同的精细程度的。我们发现需要引入
显式的同步,来确保在并发的系统中的事件的一个精确的顺序。因此,在支持函数式的风格的尝试中,
从不同的个体合并输入的需要重新引入了函数式风格已经消除了的问题。

我们开始这一章以构建计算性对象的目标,它的结构匹配了我们的真实世界的视角。我们能建模世界
为一个单独的,绑定时间的交互性的对象,对象有状态,或者我们能建模世界为一个没有时间,没有
状态的单位。任何一个视角有强有力的优势,但是没有哪个视角能单独完全满意。一个大一统的并没
有出现。

猜你喜欢

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