第一章 构造过程抽象
本章主要讲述程序运行过程的共通抽象部分:
- 包括程序设计的基本元素
- 过程循环的抽象(迭代和递归)
- 抽象过程传递给过程(高阶函数)
1.1 程序设计的基本元素
每一种编程语言的设计,都应该具备三种最基本的机制:基本表达式,组合的方法,抽象的方法。
所有程序设计中,我们需要关心的永远只有数据和过程。通俗的讲,平时编写代码,无非就是变量和函数两个部分组成,函数就是过程,用以操作数据,最后达成我们想要的效果。就前端来说,不管你是写css样式,还是js代码,最终都是把一个具量化的结果,传递到显示终端(浏览器)当中,显示渲染。
表达式
基本表达式实质就是最简单的词法单元,也就是碎片化的过程。比如在 scheme 中,输入512
,编译器返回512
,这就是一个最基本的表达式。书中通过本节简单介绍了 scheme 基本语法。
;; (运算符 被运算单元 运算单元 )
(+ 10 20)
(- 10 20)
(* 20 30)
(/ 20 30)
;; 括号的语法使得运算优先级很明确
(/ 20
(+ 20 30)
(- 10 (* 20 30)))
命名和环境
最简单的抽象方法,就是给予一个对象命名,通过使用名字的方法去进行计算,就是程序设计当中俗称的变量。在我的理解当中变量的存在是为了方便代码的复用和程序的可读性,在日常代码过程当中,变量命名除了一眼就能知道它所代表的的意义外,还要符合变量种类的命名规范(比如常量使用全大写,类名使用大写开头),有效率的传达到多种意义。
变量所能访问到的上下文位置,称为环境,也就是我们常说的作用域。
scheme 当中,使用 (define 变量名 变量值)
的方式来进行变量的声明。值得一提的是,scheme 中变量是没有赋值方法的。也就是说,变量永远当做常量来进行使用。
(define PI 3.1415926)
PI
;; 3.1415926
复合过程
定义过程是一种强大的抽象过程,合理的程序设计当中应该存在。定义过程实际上就是一段表达式的复用。scheme中我们使用define来定义一个过程(define (name parameters) body)
。
;; 这里我们定义一个求放入两个参数的平方和的过程(函数)
(define (sum-square par1 par2)
(+
(* par1 par1)
(* par2 par2)))
;; 使用函数
(sum-square 1 2)
;; 等同于1 + 4, 5
过程应用的代换模型
过程的抽象解释,分为正则序和应用序两种模型。正则序的特征为完全展开后规约,而应用序为先求值参数而后规约。
正则序和应用序是程序解释器两种解释顺序模型的简单体现。
下面是刚才sum-square
函数正则序和应用序的示例。
;; 调用 sum-square
(define par1 10)
(sum-square par1 (+ 10 20))
;; 正则序代换
;; 1
(sum-square par1 (+ 10 20))
;; 2
(+
(* par1 par1)
(* (+ 10 20) (+ 10 20))))
;; 3
(+
(* 10 10)
(* (+ 10 20) (+ 10 20))))
;; 应用序代换
;; 1
(sum-square 10 (+ 10 20))
;; 2
(sum-square 10 30)
;; 3
(+
(* 10 10)
(* 30 30)))
条件表达式和谓词
条件表达式是增强过程谓词的关键。根据不同的情况执行不同的过程步骤是必要的功能。
scheme中有if
和cond
两种条件语句。
;; cond 适用多分支的情况
(cond (p e)
(p e)
(else e))
;; if 适用于两种情况的模式
(if p e elseE)
;; 这里用大于0时候加上2,否则减去2为例书写一下 cond 和 if
;; 这儿为了展示cond的多分支特性,特意书写=0的情况
(cond ((> x 0) (+ x 2))
((= x 0) (+ x 2))
(else (- x 2))
)
(if (< x 0)
(- x 2)
(+ x 2)
)
scheme还有三个基础的逻辑复合运算符。and not or
。运算返回布尔值#t #f
。and为一假为假,全真为真。or为一真为真,全假为假,not为取相反。
(and e1 ... e2)
(not e1)
(or e1 ... e2)
过程作为黑箱抽象
过程的封装需要有黑箱的抽象概念,即任何使用过程的用户,只需要关心输入,和得到的输出,不要去关心其他任何的东西。为此,我们可以把过程所用到的东西,封装在过程当中,唯一露出给使用者的就是他所输入的函数。这种结构称之为块结构。
这里,我们设计一个函数isEven
,用户输入一个数字,判断这个数字是否是偶数,是的话反而真,不是返回false。
;; 函数的思路是一直减少2,直到小于2为止,如果为0,那么是偶数,否则为奇数
;; 用户只需要关心输入的 isEven 以及它的返回值 #t #f
(define (isEven testNum)
;; 定义过程,输入一个数字 返回它减2的值
;; 这个减2的过程封装在 isEven 当中
(define (sub2 num) (- num 2))
;; 递归,如果没有小于2继续
(if (< testNum 2) (= testNum 0)
(isEven (sub2 testNum))
)
)
1.2 过程与它们所产生的的计算
这里一小节将重点研究递归循环过程的两种形态,以及如何计算它们的运算复杂度(算法的概念)。
线性的递归和迭代与树形递归的比较
线性的递归,即每一步平行复杂度的递归方式。这里使用一个求数阶乘的函数来说明。
;; factorial 输入一个n 求n的阶乘 n! 即 n * (n - 1) * ... 1
(define (factorial n)
;; calnum 当前计算的值
;; counter 当前计算到哪一个数字
;; max-count 求乘阶的值
(define (fact-iter calnum counter max-count)
(if (> counter max-count)
calnum
(fact-iter
(* calnum counter)
(+ counter 1)
max-count)))
(fact-iter 1 1 n)
)
上面的factorial
函数,在输入6的时候使用应用替换模型进行展开的话,不难发现每一步都是平行复杂度的的。
(factorial 6)
(fact-iter 1 1 6)
(fact-iter 1 2 6)
(fact-iter 2 3 6)
(fact-iter 6 4 6)
;; ...以下省略
整个运算过程如同一条直线,因此是线性的递归思路。这种迭代写法的特征就是会用一个专门的值去保存需要返回计算的值,即累计器的概念。
下面我们看一下树形迭代的概念,用树形迭代的思想来重写factorial
函数。
;; factorial-treeway 树形迭代求阶乘
(define (factorial-treeway n)
;; counter 当前计算到的值
;; max-count 求乘阶的值
(define (fact-iter counter max-count)
(if (= counter max-count)
max-count
(* counter (fact-iter (+ counter 1) max-count))))
(fact-iter 1 n))
我们把factorial-treeway
用应用序展开。
(factorial-treeway 6)
(fact-iter 1 6)
(fact-iter (* 1 (fact-iter 2 6)) 6)
(fact-iter (* 1 (* 2 (fact-iter 3 6))) 6)
(fact-iter (* 1 (* 2 (* 3 (fact-iter 4 6)))) 6)
;; 括号看得我脑壳有点疼,以下过程省略
我们不难看到树型迭代是一个展开的过程,而不是像线性递归一样,每一步都是平行前行的。树形迭代的特点就是没有一个特定的变量去保存函数需要返回的值,而是通过到递归终止条件,去进行一个展开的过程。
增长的阶
为了比较两种递归的方法差异,这里书本引入了θ
记资源消耗的概念,其实这儿就是算法复杂度的思想。
我们需要一个抽象的概念去衡量一个程序消耗的资源程度,来规避一些影响条件(如计算机硬件)。这里的θ
就相当于算法中的O表示法,表示一步程序运行所消耗的资源。比如(+ 1 2)
,这里执行了一步程序操作,1+2
。这里的消耗就是θ(1)
。记总消耗为f(n)
,消耗了n步
,那么这个公式就可以写成f(n)=θ(n)
。
具体算法方面的内容,我在 github 上面用node
写了一个学习的库,感兴趣的可以去看一下,这里就不展开讨论了。
1.3 高阶函数做抽象
这一章主要讲述了高阶函数的函数式编程的概念。所谓高阶函数,就是能把函数作为参数传递给一个函数,并且还能作为返回值来返回一个函数。React
当中的高阶组件,其实就是高阶函数的思想组件版。
以过程作为参数,返回一个过程,这个过程称为高阶过程。这种强有力的抽象机制,极大的增强语言的表述能力。
scheme中为了方便过程的传递,创造了lambda
和let
的方式来方便进行函数的传递。下面我们来定义一个函数,功能类似于for循环,它能够接收一个过程和一个区间范围,返回过程计算的区间范围的值。
;; each 循环的过程
;; cal 累计器的初始值
;; a 起始点
;; b 结束点
(define (eachInterval each cal a b)
;; a小于等于b的时候进行循环操作
(if (not (> a b))
(eachInterval each (each a cal) (+ a 1) b)
cal
)
)
我们可以使用 lambda
来方便的传递过程给 eachInterval
。lambda
使用的方式是(lambda (parms) body)
。
;; 计算1+100的值,虽然我们都知道是5050
(eachInterval (lambda (nowIdx cal) (+ cal nowIdx)) 0 1 100)
scheme中还可以使用let
来创建一个结构代码块。注意,let
创建的变量仅在let
结构块中生效。
(let (
(val1 exp1)
(val2 exp2)
) body)
;; 这里简单说明一下作用域的例子
(define x 10)
(let (
(x 3)
;; 此时y赋值时引用的x仍为外部定义的x
(y (+ x 2))
;; let 定义的 x 和 y 仅在 body体 当中生效!在参数定义部分使用的话仍然使用的是全局变量的值
) (* x y))
总结
其实本质上,第一章是在介绍 scheme 写法的基本要素。变量声明,定义函数,循环的用法以及高阶函数的用法。但是本书通过抽象的思想把scheme的这些基本语法,站在程序的设计共通性的高度上讲解了出来。这就是这本书高明的地方。编程修炼的内功,就是指这些思考吧。