您现在的位置是:首页 >技术教程 >《计算机构造与解释》读书笔记(6)网站首页技术教程
《计算机构造与解释》读书笔记(6)
1. 写在最前面
「利用输出,来倒逼自己学习,大概是倦怠期最好的解决方法了吧」,最近查的问题有点多,每天略显心力交瘁,学习新知识的速度要提升呀。
查问题小心得,如果发现一个问题,在多个客户的使用中被反复提及,请务必提高警惕,这个问题即为—「产品设计的不合理的体现」。
注:研发大量的人肉运维,始终不如发现根因后,修复产品设计的方法,更能提高用户的使用满意度
警惕大量的「重复体力劳动」,让自己丧失深入思考的动力!
2. 元语言抽象
专业程序员在设法控制他们的设计的复杂性时,采用的正是与所有复杂系统的设计者同样的通用技术。将基本元素组合起来,形成复合元素,从复合元素出发通过抽象形成更高一层的构件,保持系统的模块性。
注:建立新语言是在工程设计中控制复杂性的一种威力强大的工作策略,常常能通过采用一种新语言而提升处理复杂问题的能力,因为新语言可能是我们一种完全不同的方式,利用不同的原语,不同的组合方式和抽象方式去描述所面对的问题。
元语言抽象就是建立新的语言。它在工程设计的所有分支中都扮演着重要的角色,在计算机程序设计领域更是特别重要,因为这个领域中,我们不仅可以设计新的语言,还可以通过构造求值器的方式实现这些语言。
注:求值器决定了一个程序设计语言中各种表达式的意义,而它本身也不过就是另一个程序。
处理大规模计算机系统的技术,与构造新的程序设计语言的技术有紧密的联系,而计算机科学本身不过就是有关如何构造适当的描述语言的学科。
在这一章里,我们将用 Lisp 语言作为基础,将各种求值器实现为一些 Lisp 过程。
3. 元循环求值器
「用与被求值的语言同样的语言写出的求值器被称为元循环」,从根本上来说,元循环求值器也就是 3.2 节所描述的求值的环境模型的一个 Scheme 表达形式,该模型包括两个部分:
-
在求值一个组合式时,首先求值其中的子表达式,而后将运算符子表达式的值作用于运算对象子表达式的值
-
在将一个复合过程应用于实际参数时,我们在一个新的环境里求值这个过程的体。构造这一环境的方式就是用一个框架扩充该过程对象的环境部分,框架中包含的是这个过程的各个形式参数与这一过程应用的各个实际参数的约束
这两条规则描述了求值的核心部分,也就是它的基本循环。这一求值循环实际体现为求值器里的两个关键过程 eval 和 apply 的相互作用。
3.1 求值器的内核
eval
eval 的参数是一个表达式和一个环境。eval 对表达式进行分类,依此引导自己的求值工作。
注:eval 的构造就像是一个针对被求值表达式的语法类型的分情况分析。
apply
apply 有两个参数,一个是过程,一个是该过程应该去应用的实际参数的表。apply 将过程分为两类。它直接调用 apply-primitive-proceduce 去应用基本过程,应用复合过程的方式是顺序地求值组成该过程体的那些表达式。
过程参数
eval 在处理过程应用时用 list-of-values 去生成实际参数表,以便完成这一过程应用。
条件
eval-if 在给定环境中求值 if 表达式的谓词部分,如果得到的结果为真,eval-if 就去求值这个 if 的推论部分,否则它就求值其替代部分。
序列
eval-sequence 用在 apply 里,用于求值过程体力的表达式序列。
赋值和定义
下面过程处理变量赋值。它调用 eval 找出需要赋的值,将变量和得到的值传给过程 set-variable-value!,将有关的值安置在指定的环境里。
3.2 表达式的表示
这两个程序完成的都是一些对符号表达式的操作。
-
在两个程序里,对于一个复合表达式的操作结果,也都是由表达式的片段递归地确定的,结果的组合也是按照一种由表达式的类型确定的方式。
-
在这两个程序里,我们都采用了数据抽象技术,借以解耦一般性的操作规则与表达式特定表示的细节方式之间的联系。
例子:
-
在微分程序里,这意味着同一个微分过程可以处理前缀形式、中缀形式,或者其他形式的代数表达式
-
对于求值器,这意味着,被求值语言的语法形式可以仅仅由一些对表达式进行分类和提取表达式片段的过程确定
派生表达式
一些特殊形式可以基于其他特殊形式的表达式定义出来,而不必直接去实现。一个这样的例子是 cond,它可以实现为一些嵌套的 if 表达式。举例来说,我们可以将对于下述表达式的求值问题:
(cond ((> x 0) x)
((= x 0) (display 'zero) 0)
(else (- x)))
归约为对下面涉及 if 和 begin 的表达式的求值问题:
(if (> x 0)
x
(if (= x 0)
(begin (display 'zero)
0)
(- x)))
采用这种方式实现去 cond 的求值能简化求值器,因为这样就减少了需要特别描述求值过程的特殊形式的数目。
3.3 求值器数结构
除了需要定义表达式的外部语法形式之外,求值器的实现还必须定义好在其内部实际操作的数据结构,作为程序执行的一部分。例如,定义好过程和环境的表示形式,真和假的表示方式等等。
谓词检测:为了实现条件表达式,我们把除了 false 对象之外的所有东西都接受为真
过程的表示: 为能处理基本过程,我们假定已经有了下述过程:
-
(apply-primitive-procedure <proc> <args>)它将给定的过程应用于表 <args> 里的参数值,并返回这一应用的结果。
-
(primitive-procedure ? <proc>)检查 <proc> 是否为一个基本过程
对环境的操作:求值器需要一个对环境的操作,一个环境就是一个框架的序列,每个框架都是一个约束的表格,其中的约束关联起一些变量和与之对应的值。
注:本章所描述的方法,只不过是表示环境的许多可能方法之一。由于前面采用了数据抽象技术,将求值器的其他部分与这些表示细节隔离开,如果需要的话,我们也完全可以修改环境表示。
3.4 作为程序运行求值器
将求值器描述为程序的一个优点是我们可以运行这个程序,这样就给了我们一个能够在 Lisp 里运行的,有关 Lisp 本身如何完成表达式求值的工作模型。这一模型可以作为一个工作框架,使人能够去试验各种求值规则。
注:求值器程序最终将把表达式归约到基本过程的应用。
每个基本过程名必须有个约束,以便当 eval 求值一个应用基本过程的运算符时,可以找到相应的对象,并将这个对象传给 apply。为此我们必须创建一个初始环境,在其中建立起基本过程的名字与一个唯一对象的关联,在求值表达式的过程中可能遇到这些名字。
注:这一全局环境里还要包含符号 true 和 false 的约束,这就使它们也可以作为变量用在被求值的表达式里。
(define (setup-environment)
(let ((initial-env
(extend-environment (primitive-procedure-names)
(primitive-procedure-objects)
the-empty-environment)))
(define-varibale! 'true true initial-env)
(define-varibale! 'false false initial-env)
initial-env))
(define the-global-environment (setup-environment))
注:基本过程对象的具体表示并不重要,只要 apply 能识别它们,并能通过过程 primitive-procedure? 和 apply-primitive-procedure 去应用它们。
3.5 将数据作为程序
在思考求值 Lisp 表达式的 Lsip 程序时,有一个类比可能很有帮助。关于程序意义的一种操作观点,就是将程序看成一种抽象的(可能无穷大的)机器的一个描述。例如,考虑下面这个已经非常熟悉的求阶乘程序:
(define (factorial n)
(if (= n 1)
1
(* (factorial (- n 1)) n )))
求值器的另一惊人方面,在于它就像是在我们的程序设计语言所操作的数据对象和这个程序设计语言本身之间的一座桥梁。
现在设想这个求值程序正在运行,一个用户正在输入表达式并观察所得到的结果。
-
从用户的观点看,他所输入的形如(* x x)的表达式是程序设计语言里的一个表达式,是求值器将要执行的东西。
-
而从求值器的观点看,这一表达式不过是一个表(在目前的情况下,是三个符号 *、x 和 x 的表),它所要去做得,也就是按照一套良好的规则去操作这个表。
3.6 内部定义
求值和元循环求值器的环境模型将按顺序执行给它的定义,一次在环境框架里扩充一个定义。对于交互式的程序开发,这样做事特别方便的,因为程序员需要自由地混合过程应用和新过程的定义。
注:但是用于实现块结构的内部定义,就会发现,这种一次一个名字的环境扩充方式可能不是定义局部变量的最好方式。
考虑如下一个带有内部定义的过程:
(define (f x)
(define (even? n)
(if (= n 0)
true
(odd? (- n 1))))
(define (odd? n)
(if (= n 0)
false
(even? (- n 1)))
<f 体的其余部分>)
在这里的意图是,在过程 even? 的体里的名字 odd? 应该引用过程 odd?,而它是在 even? 之后定义的。名字 odd? 的作用域是 f 的整个体,而不仅是 f 的体里从出现 odd?的 define 点开始的那个部分。确实,在我们考虑 odd?本身也是基于 even? 定义的时候——所以 even? 和 odd? 是相互递归定义的过程——就可以看到。
注:有关这两个 define 的最令人满意的解释,应该是认为两个名字 even? 和 odd? 被同时加入环境中。
总结,在块结构里,一个局部名字的作用域,应该是相应 define 的求值所在的整个过程体。
事实上,只要过程里的内部定义出现在体和用于定义变量的值表达式的求值之前,而这些值表达式又不实际使用任何被定义变量,顺序求值机制给出的结果就与直接实现同时定义完全一样。
3.7 将语法分析与执行分离
上述实现的求值器确实很简单,但却也非常低效,因为有关表达式的语法分析与它们的执行交织在一起。如果一个程序要执行很多次,对于它的语法分析也就需要做许多次。
可以对求值器做一些变换,使其效率大大提高,采用的方法就是重新安排其中的工作,使有关的语法分析只进行一次。
-
要将以一个表达式和一个环境为参数的 eval 分割为两个部分
-
过程 analyze 只取表达式作为参数,它执行语法分析,并返回一个新的过程,执行过程,其中封装起在分析表达式的过程中已经完成的工作。
将语法分析和执行分开之后,eval 现在就变成了
(define (eval exp env)
((analyze exp) env ))
调用 analyze 的结果得到一个执行过程,而后将它应用于环境。analyze 过程完成 3.1 里 eval 那样的分情况分析,除了其中的分派过程只执行分析工作,不进行完全的求值:
(define (analyze exp)
(cond ((self-evaluating? exp)
(analyze-self-evaluating exp))
((quoted? exp) (analyze-quoted exp))
((variable? exp) (analyze-variable exp))
((assignment? exp) (analyze-assignment exp))
((definition? exp) (analyze-definition exp))
((if? exp) (analyze-if exp))
((lambda? exp) (analyze-lambda exp))
((begin? exp) (analyze-sequence (begin-ations exp)))
((cond? exp) (analyze (cond->if exp)))
((appliction? exp) (analyze-appliction exp))
(else
(error "Unknown expression type -- ANALYZE" exp))))
下面是一个处理自求值表达式的简单分析过程,它返回一个忽略环境参数的执行过程,直接返回相应的表达式:
(define (analyze-self-evaluation exp)
(lambda (env) exp))
4. 碎碎念
断断续续的才把整理某一小节的文章写完,看来周末加班真的会降低生产力。
发现快三十岁的自己,还是没办法很好的控制情绪。听到要好小伙伴离职的消息,依然会不顾他人诧异的目光,在路上就开始流眼泪。但是那又有什么关系呢?大概所有人终其一生,都在和自己和解吧。
-
既然相遇是偶然,又何必在意分别时的突然呢?
-
太敏感了,被小小忽略一下,就能闷闷不乐一整天。
-
请你要发光,而不是被照亮。