越是浮躁的时候,越是需要想办法让自己从这种心境中脱离。
有效的程序需要一些组织原则,以指导系统化地完成整体设计。比如,使系统能够「自然地」划分为一些具有内聚力的部分,使其可以分别进行开发和维护。
注: 一种非常强有力的设计策略,基于被模拟系统的结构去设计程序结构。有关的物理系统里的每个对象,构造一个与之对应的计算对象;该系统里的每种活动,定义一种符号操作。
关于世界的常规观点之一,就是将它看做聚集在一起的许多独立对象,每个对象都有自己随着时间变化的状态。所谓一个对象「有状态」,也就是说它的行为受到它的历史影响。
注:例如一个银行账户就具有状态,对问题「我能取出 100 元钱吗?」的回答依赖于它的存入和支取的交易历史。
在一个由许多对象组成的系统里,其中的这些对象极少会是完全独立的。每个对象都可能通过交互作用,影响其他对象的状态,所谓交互就是建立起一个对象的状态变量与其他对象的状态变量之间的联系。
注: 要使模型成为模块化的,就要求它能分解为一批计算对象,使它们能够模拟系统里的实际对象。每一个计算对象必须有它自己的一些「局部状态变量」,用于描述实际对象的状态
通过程序设计语言里常规的符号名称去模拟状态变量,那么语言里就必须提供一个赋值运算符,使能用它去改变一个名字相关联的值。
将 「set !」与局部变量相结合,形成了一种具有一般性的程序设计技术,将使用这种技术去构造带有局部状态的计算对象。
一旦在语言里引用了赋值,代换就不再适合作为过程应用的模型了。
注:代换模型,将过程的形式参数用对应的值取代之后求值这一过程的体。
将系统看作是带有局部状态的对象,也是一种维护模块化设计的强有力技术。
与所有状态都必须显式地操作和传递额外参数的方式相比,通过引入赋值和将状态隐藏在局部变量中的技术,我们能以一种更模块化的方式构造系统。
一旦引进了「set !」和变量的值可以变化的想法,一个变量就不在是一个简单的名字了。现在的一个变量索引着一个可以保存值的位置,而存储在那里的值也是可以改变的。
注:只要不使用赋值,以同样参数对同一过程的两次求值一定产生出同样的结果。不使用任何赋值的程序设计称为函数式程序设计
引入赋值的代价,远远不是简单地打破一个特定计算模型那么简单。一旦将变化引进到计算模型,许多非常简单明了的概念现在都变得有问题了。
如果一个语言支持在表达式里「同一个东西可以相互替换」的观念,这样替换不会改变有关表达式的值,这个语言就称为是具有引用透明性。在计算机语言里包含了「set !」之后,也就打破了引用透明性,就使确定能够通过等价的表达式去简化表达式变成了一个异常错综复杂的问题。
注:一般而言,只能用如下方式确定两个看起来同一的事物是否确实是「同一个东西」:改变其中的一个对象,去看另一个对象是否也同样的改变了。
但是,如果不通过观察「同一个」对象两次,看看一次观察中看到的某些对象性质与另一次不同,又怎么能说清楚一个对象是否「变化」了呢?
与函数式程序设计相对应的,广泛采用赋值的程序设计被称为命令式程序设计。除了会导致计算模型的复杂性之外,以命令式风格写出的程序还容易出现一些不会在函数式程序中出现的错误。
注:带有赋值的程序将强迫人们去考虑赋值的相对顺序,以保证每个语句所用的是被修改变量的正确版本。
一个环境就是框架的一个序列,每个框架里包含着一些约束的一个表格,这些约束将一些变量名字关联于对应的值。每个框架还包含着一个指针,指向这一框架的外围环境。
一个变量相对于某个特定环境的值,也就是在这一环境中,包含着该变量的第一个框架里这个变量的约束值。如果在序列中并不存在这一变量的约束,就说这个变量在该特定环境中是无约束的。
注:环境对于求值过程是至关重要的,因为它确定了表达式求值的上下文。在一个程序语言里的一个表达式本身根本没有任何意义。即使像(+ 1 1)这样极其简单的表达式,其解释也要依赖于有关的操作是在某个上下文里进行的,在那里 + 是表示加法的符号。
如果要对一个组合表达式求值
环境模型说明:在将一个过程应用于一组实际参数时,将会建立起一个新环境,其中包含了将所有形式参数约束于对应的实际参数的框架,该框架的外围环境就是所用的那个过程的环境。
注:虽然这一求值模型比较抽象,但它却为解释器对于表达式求值的过程提供了一个正确的描述。
思考组合式 (f 5) 怎么求值得到 136:
(define (square x)
(* x x))
(define (sum-of-squares x y)
(+ (square x)(square y)))
(define (f a)
(sum-of-squares (+ a 1)(* a 2)))
求值过程如下图:
注:此处关系的是环境结构,因此不详细考察这些返回值如何在调用之间传递的问题
从环境模型出发,用过程和赋值表示带有局部状态的对象,以下面的创建「提款处理器」为例:
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
构造如下两个实例:
(define W1 (make-withdraw 50))
(define W2 (make-withdraw 100))
W1 和 W2 是两个不同的过程对象,W1 和 W2 具有相同的代码,即在 make-withdraw 内的 lambda 表达式所确定的代码,但是却具有者不同的对 balance 的局部约束。
环境模型已经解释清楚了以局部过程定义作为程序模块化的有用技术中的两个关键性质:
为了模拟具有不断变化的状态的复合对象,需要设计出阈值对应的数据抽象,使其不但包含选择函数和构造函数,还包含一些称为「改变函数」的操作,这种操作能修改有关的数据对象。
注:改变函数能够极大地提升序对的表达能力,使其能构造出序列和树之外的其他数据结构。
针对序对的基本操作— cons、car 和 cdr — 能用于构造表结构,或者选出表结构中的各个部分,但它们不能修改表结构。
注:至今用过的其他表结构,比如 append 和 list 也都是如此,它们都可以基于 cons 、car 和 cdr 定义出来。
要修改表结构就需要新的操作,针对序对的基本改变函数是 set-car! 和 set-cdr!。cons 通过创建新的序对的方式构造新的表,而 set-car! 和 set-cdr! 则是需改现存的序对。
由于引入赋值而产生的「同一」和「变化」的理论问题,当不同的数据对象共享某些序对是,这些问题就表现到现实中来了。
注:如果只用 cons、car 和 cdr 对各种表进行操作,其中的共享完全不会被察觉。然而,如果允许改变表结构的话,共享的情况就会显现出来。
检查表结构是否共享的一种方式是使用谓词 eq?,比如(eq? x y)检查 x 和 y 是否为同一个对象,即 x 和 y 作为指针是否相等。
利用共享结构可以极大的扩展能够用序对表示的数据结构范围。在另一方面,共享也可能带来危险,因为对这种结构的修改将会影响那些恰好共享这被修改了的序对的结构。
从理论上说,为了表现变动数据的行为,所需要的全部东西也就是赋值。只要将赋值纳入这一语言,就引出了所有的问题,不仅是赋值,而且也包括一般性的变动对象。
利用改变函数 set-car! 和 set-cdr!,可以用序对构造出一些单靠 cons、car 和 cdr 无法构造的数据结构。本节将展示用序对表示一种称为序对的数据结构。
一个队列是一个序对,数据项只能从一端插入(队尾)只能从另一端删除(队头),按照数据抽象的说法,队列可以看作是由下面一组操作定义的数据结构:
队列引入了一个问题是为找到表尾需要扫描整个表。如果要避免这一缺陷,那就需要修改表示方式,将队列表示一个表,并带有一个指向表的最后序对的指针。
每个值保存在一个关键码之下,将这种表格实现为一个记录的表,其中的每个记录将实现一个关键码和一个关联值组成的序对。将这种记录连接起来构成一个序对的表,这些作为连接结构的序对就成为这一表格的骨架。
一维表格的示例图如下:
两维表格里的每个值由两个关键码索引。将这种表格构造为一个一维表格,其中的每个关键码又标识了一个子表格。
注:在需要查找一个数据项时,先用第一个关键码确定对应的子表格,然后第二个关键码在这个子表格里确定记录。
上面定义的 lookup 和 insert! 操作都以表格作为一个参数,这可以将它们用到包含多个表格的程序里。处理多个表格的另一种方式是为每个表格提供一对独立的 lookup 和 insert! 过程。
设计复杂的数字系统,例如计算机,是一种非常重要的工程活动。数字系统都是通过连接一些简单元件构造起来的。虽然这些元件单独看起来功能都很简单,它们连接起来形成的网络就可能产生非常复杂的行为。
注:本节将设计一个执行数字逻辑模拟的系统,这一系统是通常被称为事件驱动的模拟程序的一个典型代表。
可以将一些基本功能部件连接起来,构造出更复杂的功能。比如下图的半加器电路,其中包括一个或门、两个与门和一个反门。
注:做出这种稍复杂组件的好处是,可以基于其作为基本构建,去创建更复杂的电路,比如基于半加器去构造全加器
从本质上看,模拟器提供了一种工具,作为构造电路的一种语言。各种基本功能块形成了这个语言的基本元素,将功能块连接起来就是这里的组合方法,而将特定的连接模式定义为过程就是这里的抽象方法。
基本功能块实现一种「效能」,使得在一根连线上的信号变化能够影响其他连线上的信号。为了构造出这些功能块,需要连线上的如下操作:
利用这些过程,就可以定义基本的数字逻辑功能。
在这种模拟中,一条线路也就是一个具有两个局部状态变量的计算对象:其中一个是信号值 signal-value(其初始值取 0),另一个是一组过程 action-procedures,在信号改变时,这些过程都需要运行。
注:一条线路被所有连接在该线路上的各种设备所共享。这样,由一个设备交互所造成的变化就会影响到连接在这条线路上的其他设备。
为完成这一模拟器,剩下的就是 after-delay。这里的想法是维护一个称为待处理表的数据结构,其中包含着一个需要完成的事项清单。对于这个待处理表,定义如下操作:
下面过程中将一个「监视器」放在一个线路上,用于显示模拟器的活动。这一过程告诉相应的线路,只要它的值改变了,就应该打印出新的值,同时打印当前的时间和线路名字:
(define (probe name wire)
(add-action! wire
(lambda()
(newline)
(display name)
(display " ")
(display (current-time the-agenda))
(display " New-value = ")
(display (get-signal wire)))))
介绍待处理表数据结构的细节,这一数据结构里面保存着已经安排好,将在未来时刻运行的那些过程。这种待处理表由一些时间段组成,每个时间段是由一个数值(表示时间)和一个队列组成的序对,在这个队列里,保持着那些已经安排好的,应该在这一段时间运行的过程。
在传统上,计算机程序总被组织成一种单向的计算,它们对一些事先给定的参数执行某些操作,产生所需要的输出。但在另一方面,也经常需要模拟一些由各种量之间的关系描述符系统。
比如某个机械结构的数学模型里可能包含着这样的一些信息:在一个金属杆的偏转量 d 与作用于这个杆的力 F、杆的长度 L、截面面积 A 和弹性模数之间的关系可以由下面的方程描述: dAE = FL
注:这种关系并不是单向的,给定其中的任意的 4 个量,就可以利用它计算第 5 个量。
本节要描绘一种语言的设计,这种语言可以基于各种关系进行工作。这一语言里的基本元素就是「基本约束」,它们描述了不同量之间的某种特定关系。通过组合各种基本约束,以便于去描述更复杂的关系。
注:例如 (adder a b c)描述的是量 a、b 和 c 之间必须有关系 a + b = c
例子,在华氏温度和摄氏温度之间的关系是:9C = 5(F - 32)
,为了使用约束的系统模型去执行温度计算,需要首先调用构造函数 make-connector,创建起两个连接器 C 和 F,然后将它们连接到一个适当的网络里:
(defile C (make-connector))
(defile F (make-connector))
(celsius-fahrenheit-converter C F)
OK
创建上述网络的过程定义如下:
(define (celsius-fahrenheit-converter C F)
(let ((u (make-connector))
(v (make-connector))
(w (make-connector))
(x (make-connector))
(y (make-connector))
(multiplier c w u)
(multiplier v x u)
(adder v y f)
(constant 9 w)
(constant 5 x)
(constant 32 y)
'ok))
这一过程建立起内部连接器 u、v、w、x 和 y,调用基本约束的构造函数 adder、multiplier 和 constant ,并按照定义将它们连接起来。
约束系统用具有内部状态的过程对象实现。
注:虽然约束系统里的基本对象在某些方面更复杂一些,但整个系统却更为简单,因为这里完全不需要关心待处理表和时间延迟等等问题。
连接器的基本操作包括:
连接器用带有局部状态变量 value、informant 和 constraints 的过程对象表示,value 中保持这个连接器的当前值,informant 是设置连接器值的对象,constraints 是这一连接器所涉及的所有约束的表。
断断续续的看了一周也只记录完成一周的 3/5 ,我只能说经典的书籍被称之为经典不是木有原因的。希望在年前能把第三章全部看完吧(ps: 发现最近接触过的人确诊阳性的越来越多了,这大概是我离确诊新冠最近的一次