第三章:模块化,对象与状态
第三章:模块化,对象与状态
导言:
构造大型程序的一种很有力的构造策略,就是去模拟真实世界的物理系统,基于被模拟系统的结构去设计自己程序的结构。本章主要提出了两种不同的对世界的看法,由此也引申出了两种组织程序结构的策略。
第一种是把注意力集中到“对象”上,把一个大型系统看成一大批的对象,其行为或状态随时间会发生变化。另一种方法是把注意力集中到流过系统的信息流上,就如同处理一个信号处理系统。基于这些看法,我们又会引出一些新的数据结构,一些新的处理技术比如赋值与延时求值。
3.1赋值与局部状态
在此之前,我们构造的过程都是良好定义的数学函数,过程执行的结果不会随着时间,或者说顺序发生改变。但这显然无法满足我们的需要。在普遍的观点中,我们把世界分解成一大批的对象,每个对象都会维持自己的状态,随着时间或者说交互而发生变换。我们可以用变量来刻画对象的状态,所谓的交互既是对象的状态变量之间的练习。要做到上述要求,我们就需要引入赋值。
本书所用的编程语言使用set!来实现赋值,而现代编程语言很多都已经可以用“="实现赋值。
在实际编程中,我们可以把状态变量封装入函数中,来实现对状态的保护,只有这个函数能够访问变量,使得系统更模块化和强健。
引入赋值给我们提供了强有力的手段,让我们设计出来的系统结构更加优良。从一个复杂计算的过程中看,程序的其他部分都像是在随着时间不断变化。通过引进赋值和将状态影藏在局部变量中的技术,我们可以用更模块化的方式构造一个系统。
但同样,赋值也对程序造成了损害。在之前,以同样参数对同一过程的两次求值会产生同样的结果,这被称作函数式编程设计。但引入了赋值后,就把我们的代换模型给破坏掉了。显然,一个值会变化的量,我们不能粗暴地直接把他代换入函数中。从本质上说,代换的基础是:语言里的符号不过是值的名字。引入了赋值,符号就不是值的一个锚,他可以随时指向另一个值。现在的变量索引着一个保存的值,而存储在那里的值也是可以改变的。由此又引申出了一个问题,在这种广泛采用赋值的程序设计中(命令式程序设计),强迫着我们去思考赋值的正确顺序,以保证每个语句使用的都是被修改变量的正确版本。
3.2:求值的环境模型
既然代换模型被打破,我们就需要引入新的模型,即环境模型。为此引入了一些新定义。
一个环境就是一个框架的一个序列(即一连串的框架)。每个框架是包含着一些约束的一个表格。这些约束将一些变量名字关联于对应的值。每个框架涵包含一个指针,指向这一框架的外围环境(外一层的框架)。
一个过程现在是一个对偶。由代码块和一个指向环境的指针组成。
然后我们就可得到环境模型的工作原理:
- 将一个过程应用于一集实际参数,将构造出一个新框架,其中将过程的形式参数约束到调用时的实际参数,然后在构造起的这一新环境中的上下文中求值过程体。这个新框架的外围环境就是作为被应用的那个过程对象的一部分的环境。
相对于一个给定环境求值一个lambda表达式,将构造一个过程对象,这过程对象是一个序对,由该lambda表达式的正文和一个指向环境的指针组成。指针指向的是创建这个过程对象时的环境。
说实话,这部分的内容很像讲的作用域啥的。
## 3.3用变动数据做模拟
在此前,我们的数据结构只用构造函数和选择函数来描述。但引入了赋值之后,我们还需要加入改变函数的操作,来模拟具有不断变化的状态的系统。
本章前部分介绍了变动的表结构,列表,队列,表格(实际上就是二维数组)等数据结构。这些部分像是数据结构里的内容,在此不多赘述。由这些数据结构也引出了一个问题:共享与相等。这一部分我认为使用C++里的指针更好理解。相等有两种:一种是指针相等,它们指向同一个对象;另一种是值相等,但它们指向不同的对象。所谓的共享值的就是第一种相等。这样来看就很显而易见了。。
后半部分有两个非常有趣的例子,一个是数字电路的模拟器,另一个是约束的传播。第一个是模拟了一个简单数电系统,第二个模拟了一个方程(等式)中的参数发生变化时如何对其他参数产生影响这两个部分我认为非常新奇有意思,尝试把它们实现出来。也发布到博客SICP学习记录中了
## 3.4:并发:时间是一个本质问题
对象不总是一次一个地顺序变化,而是并发地活动,一起活动。对于函数式编程,顺序不会造成任何影响,但引入了赋值,尤其是对不同进程之间共享的变量的赋值,其计算的结果就会依赖于各个不同赋值之间发生的顺序,显然这是很糟糕的状态。解决方法一是不允许同时修改两个变量,而是通过巧妙的安排,保证并发产生的结果按照某种方式运行的结果完全一样。
书中介绍了一种控制并发的机制,串行化组。创建一些不同的过程集合,保证在任何一个串行化集合里面至多只有一个过程的执行。如果集合中有过程正在执行,另一个程序试图执行其中的任何过程,必须等到前一个过程结束。通过这个方法来控制并发。
进而又介绍了一种互斥元的同步机制来实现串行化。互斥元提供两个操作,获取和释放。一个互斥元被获取,对互斥元的任何其他获取操作必须等到互斥元被释放。每个串行化组关联一个互斥元。给了一个过程p,串行化组返回一个过程,过程获取互斥元,运行p结束后释放。
## 3.5:流
这是一个很有意思的数据结构,用来替代赋值来进行模拟。
如果关注的是一个个时刻的变化量,我们可以把它看做一个对象。如果关注这个变化量的整个变化序列,就可以看做流。这种方法能够把处理对象推广至无穷。为此,引进一种技术,延迟求值。
流作出一种安排,部分地构造出流的结构(需要的结构),并将其送到处理流的程序,如果需要流更多的部分,那么就自动地构造流。
流的首元素是当前元素,第二个元素是延迟对象,可以看做是会对他求值作出的允诺,通过force对他求值。有点像数学归纳法。
force和delay的实现其实很简单,用过程包装起来,再执行过程
#lang sicp
(define (cons-stream a b);;构造流
(cons a (delay b)))
(define (stream-car stream);;取出流当前元素
(car stream))
(define (stream-cdr stream);;承诺会求值
( force (cdr stream)))
(define (delay stream);;用过程包装起来
(lambda()
stream))
(define (force delayed);;求值,去掉过程
(delayed))
有了流,我们可以结合起之前介绍的过滤器,累加器,枚举器,等,像信号处理系统一样处理流。我在博客的“流实现与操作”中完成了流的表示和部分有意思的应用。
显然流具有很强的威力,最明显的一点就是我们能够处理无穷了。另外,我们也能够将迭代操作表示为流过程(把保存的过程变量看做一个流)
但流也有问题。在一些共享的对象上的问题里,我们需要合并流。由于流把时间这个概念分离了出来,需要模拟的系统涉及到时间的话就很难处理。但流依然是一种很强大的工具。
还有一点在于,如果采用正则序求值表达式,完全归约后展开,显然不能使用流。这会无限递归,爆内存。