书中使用语言为Lisp方言——Scheme
1.1程序设计的基本元素
三种机制:
基本表达形式,用于表示语言所关心的最简单的个体。
组合的方法,通过它们可以从较简单的东西出发构造出复合的元素。
抽象的方法,通过它们可以为复合对象命名,并将它们当作单元去操作。
两类要素:
过程和数据。任何强有力的程序设计语言都必须能表述基本的数据和基本的操作,并且提供对这二者进行组合和抽象的方法。在Lisp中,过程和数据的边界是模糊的,过程本身可以作为Lisp的数据来表示和操作。
1.1.1表达式
486 486
组合式:
(+ 137 349) 486
(- 1000 334) 666
(* 5 99) 495
(/ 10 5) 2
(/ 10 3) 10/3 三又三分之一
(/ 10.0 3) 3.3333333333333335
如上所示,Lisp解释器提供了+、-、*、/等操作符,组合式为前缀表示。
前缀表示的一个优点是操作符后的操作数可能接受任意个,如以上四个操作符就支持。
(/ 10 3 3) 10/9 一又九分之一
另一个优点是支持直接扩充,允许组合式嵌套的情况,即组合式的元素本身又是组合式。理论上嵌套的深度没有限制,所以Lisp的代码看起来是无数个括号套着。
(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6)) 57
而过于复杂的表达式一般采用一种美观打印的编码风格,以便看清层次关系。
(+ (* 3
(+ (* 2 4)
(+ 3 5)))
(+ (- 10 7)
6))
1.1.2 命名和环境
名字标示符称为变量,它的值是所对应的对象。Scheme中使用define给对象绑定标示符,对象可以是数值、组合式、操作符、函数,一切皆为数据。对应数值、组合式时称为命名,对应操作符、函数及其形参时称为复合过程。
(define size 2)
size 2
(define mul (* 1 2))
mul 2
(define (+ a b)
(cond ((= 0 a) b)
((> 0 a) (+ (- a -1) (- b 1)))
(else (+ (- a 1) (- b -1)))))
(+ 1 3) 4
(+ -1 3) 2
(define (abs a)
(if (< a 0)
(- a)
a))
(abs -1) 1
如上所示+定义(意义为整数相加)之后将在所在环境中替代解释器的+,如果define的内容中使用了+则是调用了这个过程本身,直到终止条件a变为0,实际上每次语法上的递归都是改变了两个参数a、b的值,所以这是计算上的迭代,也是尾递归。而将代码中的(- b -1)等四个使用-的地方改成(+ b 1)等则会出现错误,因为每次想要从一个状态进入下一个状态时,又调用了自身产生了一个新的状态,并且结果状态依赖于这个新的状态的结果,最终会递归到状态(+ 1 1),这个状态将陷入死循环,最终堆栈将溢出。比如(- b -1)改为(+ b 1)后,计算(+ a b) (a>0,b≥0)会产生状态(+ a-1 (+ b 1),所以想要得到(+ a-1 b+1)需要计算(+ b 1),如果b=0,(+ 0 1)=1,得到(+ a-1 1),然后(+ a-2 (+ 1 1)),若b≠0,则由(+ b 1)进入(+ b-1 (+ 1 1)),都将进入(+ 1 1)的状态,接下来是(+ 0 (+ 1 1)),这时候,无论是正则序还是应用序,都将进入(+ 1 1)求值自身的陷阱。当然问题不仅仅是一个(+ 1 1),任何陷入死循环的状态都将导致内存溢出。如果代码中设置了(+ 1 1)=2的情况((and (= 1 a) (= 1 b)) 2),(+ 1 2)也将如此。而给出(+ 1 2)的值3又会有(+ 1 3)死循环。那么设置((= 1 a) (- b -1))应该就解决了这一问题集合边界上的问题。而若是b<0,则将跳到else里,如若替换其中的(- b -1)为(+ b 1),则将出现类似的边界情形,这时是((= -1 a) (- b 1))。
(define (+ a b)
(cond ((= 0 a) b)
((= 1 a) (- b -1))
((= -1 a) (- b 1))
((< 1 a) (+ (+ -1 a) (+ b 1)))
(else (- (+ (- a) (- b))))))
上面是将四个-减法操作都替换成了自身+的过程,只在a取1和-1的边界情况下使用了减法-。注意(- a)这种虽然等价于(- 0 a),但是无法用+得到一个数关于0的对称数,(+ 0 (- a)),所以可以认为这里的一元操作符与二元操作符是两种不同的-。
当然,这里还有两件事要说明,一是上面我们定义了两种(+ a b),它们的输入和输出是一致的,即对于调用这个代码块的人来说,在性能相差无几时,可以不用关心内部是如何实现的,即将这个过程作为黑盒抽象。二是性能上确实是相差巨大,当后一个+的两个参数有较大值时,由于需要产生很多的递归调用,应该是树形递归,那么占用的资源将会很多,因为空间和时间复杂度都是指数级。所以千万不要写出这样的代码来使用哦!
Lisp程序通常总是由一大批相对简单的过程组成的,即大的过程调用了小的过程,大的模块调用了小的部件,各个模块组成了一个有机的系统。
而命名的符号-值的关联,意味着解释器要维护一个存储能力,以便保持有关的名字-值对偶的轨迹。这种存储被称为环境。环境分为全局环境和局部环境。比如局部环境和外部环境有相同变量名的时候,运行时变量名指向何值?形参和实参的关系?以下代码请自行看懂,看不懂的太任性。。。
(define b 2)
(define (sth a)
(define b 1)
(+ a b))
(sth 2) 3
b 2
(define (f11 a b)
(define (f12 a) (- a b))
(+ (f12 b) b))
(f11 1 2) 2,(f11 a b)=b
(define (f21 a b)
(define (f22 b) (- a b))
(+ (f22 b) b))
(f21 1 2) 1,(f21 a b)=a
上述的f12定义于f11内部的情况叫做块结构。另外在这里顺便提一下,Lisp没有变量副作用。不知道什么是副作用的可以百度。。
好了,上述废话完毕,进入下一节。。
1.1.3组合式的求值
1)求值该组合式的各个子表达式
2)将作为最左子表达式(运算符)的值的那个过程应用于相应的实际参数,所谓实际参数也就是其他子表达式(运算对象)的值。
所以求值过程是递归的,它在自己的工作步骤中,包含着调用这个规则本身的需要。
·数的值就是它们所表示的数值
·内部运算符的值就是能完成相应操作的机器指令序列
·其他名字的值就是在环境中关联于这一名字的那个对象
可以将第二种看做是第一种的特殊情况,包含在全局环境里。环境的作用就是用于确定表达式中各个符号的意义。
请注意,define是一般性求值规则的一种例外,(define x 3)并不是一个组合式,它并不是操作了x这个符号和3这个数值来得到一个结果,而是在这两者间建立一个关联并存储于上下文中。每个特殊形式都有其自身的求值规则,各种不同种类的表达式(每种有着与之相关联的求值规则)组成了程序设计语言的语法形式。Lisp的语法很简单,对各种表达式的求值规则可以描述为一个简单的通用规则和一组针对不多的特殊形式的专门规则。这些特殊形式的还有cond、if、and、or、lambda、let等等。
1.1.4复合过程
之前的(define (+ a b) ...) 和 (define (f11 a b) ...)就是复合过程,a、b为形参,调用(+ 1 2)、(f11 1 2)等的1、2为实参。
1.1.5过程应用的代换模型
·基本运算符应用实参的机制已经在解释器做好
·复合过程应用于实参,就是在将过程体中的每个形参用相应的实参取代后,对这一过程体求值
而这里的实参,在代换模型中,每次都是一个算好的值还是未计算的表达式,就是应用序和正则序的区别。两者计算过程是不同的,而可以证明对那些可以用代换模型去模拟并能产生出合法值的过程应用,正则序和应用序的结果是一样的。
应用序时,可以避免某些表达式在正则序中的重复求值,可以提高一些效率。更重要的是,在超出了可以采用替换方式模拟的过程范围之后,正则序的处理将变得更复杂的多。而在另一方面,正则序也可以成为特别有用的工具。比如流处理就是一种采用正则序的受限形式去处理明显的“无限”数据结构的方式。
1.1.6条件表达式和谓词
cond 、if,先算谓词后算序列表达式。
and、or、not,前两个类似于C中的||、&&,子表达式不一定都求值,not则是一个普通的过程。
(define (>= x y) (not (< x y)))
(define (abs x)
(cond ((>= x 0) x)
((= x 0) (abs x))
((< x 0) (- x))))
(abs 1) 1
上述代码并不会进入死循环,因为在运行时,必须求到某个分支的谓词为真或者进入else分支时,才求所在分支的那一个表达式。
1.1.7实例:采用牛顿法求平方根
数学定义是说明性的知识,而计算机程序是行动性的知识。用平方根的数学定义并不能求出一个值来,而牛顿的逐步逼近法可以求出一个近似值。求x的平方根,有个猜测y,则y和x/y的平均值是一个更加好的猜测,不断调用这一过程知道猜测足够近似。这是一个尾递归过程。
其中的good-enough?则需要考虑采用哪一种,是guess的平方与x的差值的绝对值小于某个阈值,还是guess与old_guess的比值小于某个阈值。前者不适合很小的或者很大的数。而求立方根的牛顿法只需要将improve内的计算换成相应的表达式即可。所以sqrt-iter可以上升到一种通用做法的地位,通过替换内部的算子达到不同的实际效果。
1.1.8过程作为黑箱抽象
外部不必关心过程内部实现,过程的意义应该不依赖于其作者为形参所选用的名字。
习题1.1-1.8 git://code.csdn.net/u010374912/sicp.git