『The Little Schemer』笔记
Chap.4 数字游戏
仅考虑非负整数集。基本元件add1
接受一个参数,结果为这个参数的后继数,即这个数字+1,基本元件sub1
接受一个参数,结果为这个数字的前驱数,即这个数字-1。基本元件zero?
接受1个参数,表示这个数字是否为0.
使用这三个基本元件,首先来实现两个数字的加法运算。两数字m与n相加,可以描述为数字m进行n次add1操作,而进行n次add1
操作可以用(zero? n)
与(sub1 n)
组合进行。由于+号已经是基本运算,我们使用原子o+表示我们自定义的这个加法,尝试编写此函数:
(define o+ (lambda (m n) (cond ((zero? n) m) (else (o+ (add1 m) (sub1 n))))))
这就已经违反了Scheme十诫之第一诫:问题之首必用null?
,不过对于数字,zero?
就类似于列表的null?
,类似地add1
是类似于cons那样构建列表,sub1
像cdr那样进行一般性递归。
两数字m与n相减,即对m执行n次sub1
。尝试编写'-
函数(虽然仅考虑非负整数集,不过当前o-
函数对负整数也有效):
(define o- (lambda (m n) (cond ((zero? n) m) (else (o- (sub1 m) (sub1 n))))))
看来针对数字类型,要将Scheme十诫之第一诫进行一点更新:
Scheme十诫之第一诫(ver 1.1):对一个原子列表lat递归调用时,要先询问(null? lat),对于一个数字n进行递归调用时,要先询问(zero? n)
接下来实现乘法。两个数字m与n相乘,就是累加n次的m。对于数字而言,递归时则是由o+
连接的典型元素与一般性递归,对于乘法,典型元素就是m,一般性递归则是(o* m (sub1 n))
,即:
(define o* (lambda (m n) (cond ((zero? n) 0) (else (o+ m (o* m (sub1 n)))))))
Scheme十诫之第四诫(ver 1.1):在递归时总是至少改变一个参数。该参数必须向着不断接近结束条件而改变。改变的参数必须在结束条件中得以测试:用cdr时则用null?测试结束,用sub1时则用zero?测试结束
(o* 12 3)
的值为36,其计算过程为:
(o* 12 3) = (o+ 12 (o* 12 2)) = (o+ 12 (o+ 12 (o* 12 1))) = (o+ 12 (o+ 12 (o+ 12 (o* 12 0)))) = (o+ 12 (o+ 12 (o+ 12 0))) = (o+ 12 (o+ 12 12)) = (o+ 12 24) = 36
Scheme十诫之第五诫:用+构建一个值时,总是用0作为结束代码行的值;用*构建一个值时,总是用1作为结束行代码的值;用cons构建一个列表时,总是用(quote ())作为结束行代码的值。
记全部由数字(本节仅指非负整数)构成的列表为tup,即元组Tuple。编写一个tup+
,接受将两个长度相同的tup作为参数,结果为同样长度的元组,每个元素是相应位置原tup的元素之和。例如tup1
是(3 6 9 11 4)
,tup2
是(8 5 2 0 7)
,则(tup+ tup1 tup2)
结果为(11 11 11 11 11)
。
由于限制tup1和tup2的长度需要一致,当二者都为空时,返回一个空列表,否则将两个tup的首元素相加,作为典型元素,两个tup的分别cdr作为一般性递归,使用cons构建成一个最终的列表。
(define tup+ (lambda (tup1 tup2) (cond ((and (null? tup1) (null? tup2)) (quote())) (else (cons (o+ (car tup1) (car tup2)) (tup+ (cdr tup1) (cdr tup2)))))))
改进一下,如果tup1和tup2的长度可以不一致,长度不足则认为是以0填充,例如tup1为(3 4)
,tup2为(4 6 8 1)
,新的(tup+ tup1 tup2)
将得到(7 10 8 1)
。
这时,两个表可能不同时为空,当tup1为空的时候,应返回tup2,当tup2为空的时候,应返回tup1,都不空的时候则进行典型元素和一般性递归的构建。tup1与tup2同时为空时,可以按tup1为空返回tup2,此时tup2是空表,所以逻辑上是正确的。
(define tup+ (lambda (tup1 tup2) (cond ((null? tup1) tup2) ((null? tup2) tup1) (else (cons (o+ (car tup1) (car tup2)) (tup+ (cdr tup1) (cdr tup2)))))))
再编写lt
和gt
的表达式,分别表示“less than”和“greater than”用于数字的比较。要比较两数字m,n的大小,仅用基本元件设计递归来比较二者的大小。可以对m和n执行多次sub1,直到某个数字满足zero?,当m先满足zero?,此时n仍大于0(仅考虑非负整数),说明m比n小,反之亦然。
(define lt (lambda (m n) (cond ((zero? m) #t) ((zero? n) #f) (else (lt (sub1 m) (sub1 n))))))
测试一下,(lt 2 4)的求值过程:
(lt 2 4) = (lt 1 3) = (lt 0 2) = #t
是正确的。如果用两个相等的数字进行测试(lt 2 2)
:
(lt 2 2) = (lt 1 1) = (lt 0 0) = #t
仍然得到真的结果,但实际应当为假。原因在于两个zero?判断会对结果产生影响,如果换过来的话:
(define lt (lambda (m n) (cond ((zero? n) #f) ((zero? m) #t) (else (lt (sub1 m) (sub1 n))))))
这个问题就解决了,两个数字相等时,返回值为假。类似地,编写gt
函数:
(define gt (lambda (m n) (cond ((zero? m) #f) ((zero? n) #t) (else (gt (sub1 m) (sub1 n))))))
有了lt和gt,就可以编写对于数字类型的判等函数eq?,当然eq?作为基本元件,有些Scheme语言标准中允许对数字类型进行判等,为示区别,针对非负整数来自定义的判等函数称为oeq?
(define oeq? (lambda (m n) (cond ((> m n) #f) ((< m n) #f) (else #t))))
四则运算还有一个求商运算,scheme中可以使用quotient
。我们自定义的求商函数命名为div,要求m除以n的商,即m当中含有多少个n,就是若m仍大于n,则将典型元素为1,且一般性递归推导为m-n与n求商,最终m小于n时,商为0结束递归。
(define div (lambda (m n) (cond ((< m n) 0) (else (add1 (div (o- m n) n))))))
例如(div 15 4)
的运算过程为:
(div 15 4) = (add1 (div 11 4)) = (add1 (add1 (div 7 4))) = (add1 (add1 (add1 (div 3 4)))) = (add1 (add1 (add1 0))) = (add1 (add1 1)) = (add1 2) = 3