程序构造和解释之Scheme基础语法
author:onceday date: 2022年6月5日
1.设计基本元素
任何一门语言需要关注其将简单的认识组合起来形成更复杂认识的方法,可分为三种机制:
- 基本表达形式:用于表示语言所关心的最简单的个体。
- 组合的方法:通过它们可以从较简单的东西出发构造出复合的元素。
- 抽象的方法:通过它们可以为复合对象命名,并将它们当作单元去操作。
程序设计有两类要素:过程和数据
直观来说:
- 数据是一种我们希望去操作的“东西”,而过程就是有关操作这些数据的规则的描述。
这里使用的语言为scheme
。
语法参考:Chez Scheme Version 9 User’s Guide (cisco.github.io)
1.1 表达式
456
当用键盘输入一个表达式后,解释器就会将他对这一表达式的求值结果表达出来。
精准来说,456
是由数字组成的表达式,它表示的是以10作为基数的数。
1.2 组合式
(+ 100 10)
(- 1000 220)
(<操作> 对象1 对象2 对象3 ......)
如上的表达式称为组合式,构成方式是用一对括号括起一些表达式,表示了一个过程。
最左的元素称为运算符,其他元素都称为运算对象。
求值的时候,就将运算符刻画的过程应用于有关的实际参数。
所以,定义了过程和参数,并不意味着,就会马上去求值。
1.3 命名和环境
(define size 2)
(define add (+ x y))
如上的表达式可以定义名字标识符,也就是变量,值即所定义的那个对象。
该对象可以是操作,也可以是数据。
该过程对应抽象的方法,可以通过简单的过程定义出复杂的程序。
另外一个方面,解释器必须维护某种存储能力,一遍保持有关的名字-值对偶的轨迹,这种存储称为环境,环境可以分成很多种,如全局环境,和局部作用环境。
在这种环境下,组合式可以按照以下的规则求值:
- 数的值就是他们所表示的数值
- 内部运算符的值就是能完成相应操作的机器指令序列
- 其他名字的值就是在环境中关联这一名字的那个对象
直观来说:环境就是用于确定表达式中各个符号的意义的。
1.4 过程抽象
(define (square x) (* x x))
(define (<name> <formal parameters>) (body))
body
可以是一系列的表达式,此时解释器将顺序求值这个序列中的各个表达式,并将最后一个表达式的值作为整个过程应用的值并返回它。
代换模型:
先展开,再规约求值。这种方式不太合适,但是容易理解。
(+ (square 6) (square 10))
(+ (* 6 6) (* 10 10))
(+ 36 100)
136
这种“完全展开而后规约”的求值模型称之为正则序求值。
但是现在解释器里往往采用**“先求参数再应用“的方式**,称之为应用序求值。
应用序求值先不求出运算对象的值,直到实际需要它们的值时再去做。
1.5 条件表达式
分情况分析的特殊情况——cond
:
(define (abs x)
(cond
((> x 0) x)
((= x 0) 0)
((< x 0) (- x))
(<pn> <en>)
)
)
符号cond
后面可以跟着很多称为子句的用括号括起的表达式对偶(<p>
<e>
)
<p>
是一个谓词表达式,它的被解释为真或者假。<e>
是序列表达式,可以是多个组合式组合而成。作为相应表达式对偶的返回值。
cond
后面的表达式对偶依次顺序求谓词表达式的值,直到某个谓词表达式为真。
(define (abs x)
(cond
((> x 0) x)
(else (- x))
)
)
可以使用else
用于囊括除了前面子句之外的情况。
当分情况分析只有两种情况时,可以使用符号if
:
(define (abs x)
(if (< x 0)
(- x)
x
)
)
以下是三个常见的逻辑复合运算符:
(and <e1> ...... <en>)
从左到右一个个地求值<e>
,如果某个<e>
求值得到假,这一个and表达式的值就是假,后面的那些<e>
也不再求值了。当所有的表达式为真时,这and表达式的值就是真的。
(or <e1> ...... <en>)
从左到右一个个地求值得到真,or表达式就以这个表达式的值作为值,后面的那些<e>
也不再求值了。当所有的 表达式为假时,这or表达式的值就是假的。
(not <e>)
如果<e>
求出的值是假,not表达式的值就是真的,否则其值为假。
2.过程抽象
2.1 说明性语言和描述性语言
(define (sqrt x)
(the y (and (>= y 0)
(= (square y) x)
)
)
)
上面描述了一个平方根:
x
=
y
,
且
y
≥
0
,
y
2
=
x
\sqrt{x}=y, 且y\ge0,y^2=x
x=y,且y≥0,y2=x
虽然其描述了一个数学定义,但Scheme语言无法利用它得出平方根。
这是一个普遍的特征,即描述一件事情的特征与描述如何去做这件事情之间普遍性差异的具体表现。
也可以认为是说明性的知识与行动性的知识之间的差异。
下面使用牛顿法(逐渐逼近)实现的过程抽象:
(define (sqrt_iter guess x)
(if (good_enough? guess x)
guess
(sqrt_iter (improve guess x) x)
)
)
good_enough?
是用于判断猜测值是否足够的好,这目前还没实现。
同理,improve
也是一个过程的抽象,用于进一步改进抽象值。
在该过程中,sqrt_iter
调用了自身,这是一种递归过程抽象,但这并不意味着运算是递归的。
至于这些过程抽象的实现,是不需要在意,每一层都作为一个黑箱,可解耦各层之间的关系。
假如按以下方式实现good_enough?
和improve
过程:
(define (improve guess x)
(average guess (/ x guess))
)
(define (good_enough? guess x)
(< (abs (- (square guess) x)) 0.001)
)
可以看到,在这些过程内部也有自己的参数名字。
就像大多数编程语言所做的那样,过程的意义应该不依赖于其作者为形式参数所选取的名字。
过程中的形式参数又称为约束变量,如果在一个完整的过程定义里将某个约束变量统一换名,那么这一过程的意义将不会有任何改变。其被约束的表达式范围也就是它的作用域。
如果一个变量不是被约束的,那么它就是自由变量。
2.2 块结构和词法作用域
(define (sqrt_iter guess x)
(define (improve guess x)
(average guess (/ x guess))
)
(define (good_enough? guess)
(< (abs (- (square guess) x)) 0.001)
)
(if (good_enough? guess x)
guess
(sqrt_iter (improve guess x) x)
)
)
这种嵌套的结构也被称为块结构。它可以将improve
,good_enough?
的作用域限制在sqrt_iter
内部,不至于和其他程序造成干扰。
另外一方面,诸如x
,guess
之类的变量可以变成内部的"自由变量",从而减少显示的值传递过程。
2.3 线性递归和线性迭代
;;;斐波拉切数列,树型递归计算
(define (fib n)
(cond ((= n 0) 0)
((= n 1) 1)
(else (+ (fib (- n 1)) (fib (- n 2))))))
;;;斐波拉切数列,线性迭代计算
(define (fib n)
(define (fib_iter a b count)
(if (= count 0)
b
(fib_iter (+ a b) a (- count 1))))
(fib_iter 1 0 n))
上面是斐波那契数列的两种计算方式,递归计算是一种先逐步展开而后收缩的形状,往往需要堆栈的结构进行辅助计算,优点是容易理解,便于实现。
迭代计算过程的状态可以用固定数目的状态变量描述:
- 需要一个确定的规则描述状态之间的切换
- 需要有结束的检测条件
递归算法大部分都可以写出迭代的计算过程出来。
2.4 高阶抽象-操作过程的过程(高阶过程)
建立求和抽象模型:
∑
n
=
a
b
f
(
n
)
=
f
(
a
)
+
.
.
.
.
.
.
+
f
(
b
)
\sum^b_{n=a}f(n)=f(a)+......+f(b)
n=a∑bf(n)=f(a)+......+f(b)
这种表示方法仅需要起始项a
,结束项b
,下一项(next a)
,以及项值(term a)
。就可以抽象这一过程,而不用在意具体的求和序列。
如下所示:
(define (sum term a next b)
(if (> a b)
0
(+ (term a) (sum term (next a) next b))
)
)
其中term
和next
都是作为过程来参与其中。
通俗点讲,sum
作为函数,其参数不再限定在数据变量,而是也能包括过程,那么编写起来就很灵活了。
2.5 用lambda构造过程
匿名函数(过程),无需命名,可以减少很多无意义的事情。
(lambda (<formal-parameters>) <body>)
使用方式:
((lambda (x y z) (+ x y (square z))) 1 2 3)
2.6 用let创建局部变量
(let ((<var1> <exp1>)
(<var2> <exp2>)
......
(<varn> <expn>)
)
<body>
)
(<var1> <exp1>)
是名字-表达式,当let
被求值时,每个名字将被关联于对应表达式的值。
然后这些名字(<var1>
)约束为局部变量,并求得体<body>
的值。
其等价模式为下列表达式:
((lambda (<var1> ... <varn>))
<body>)
<exp1>
......
<exp2>)
这也意味着let
表达式只是作为其基础的lambda
表达式的语法外衣罢了。
注意:有let
表达式描述的变量的作用域就是该let
的体。
另外,<exp>
的值是在let
之外计算,而且是**“并行计算的”,所以其值依赖于外部作用域**。
2.7 第一级状态(过程)
带有最少限制的元素被称为具有第一级的状态。
- 可以用变量命名
- 可以提供给过程作为参数
- 可以由过程作为结果返回
- 可以包含在数据结构中
如果能操作过程,再将过程作为结果返回,那么极大的提升抽象能力。