SICP学习笔记(1.3.2 ~ 1.3.3)
周银辉
1,Lambda
1.3.2开始部分,可能会给你造成一点点误解:认为是为了某种表达上的方便,然后在Scheme中引入的Lambda的概念。这是不对的。事实上Lambda是函数编程语言的数学基础,它是基础,是先于其他语法形式而最早出现了。个人感觉上述误解用在命令式语言C#的Lambda表达式上倒合情合理,因为我总觉得C#中的Lambda是为了某种表述方便而引入的语法糖衣,毕竟它不是函数式语言。
- lambda表达式的“alpha转换”
我们知道,函数F(x)= ax+b 与函数F(y)=ay+b 是等价的,我们只不过是将前一个函数表达式中的变量x替换成了y而已。“alpha转换”描述的就是这一“替换”操作,该替换不会影响表达式原意,比如(lambda (x)(+(* a x)b))可以通过alpha转换变成(lambda (y)(+(* a y)b))。 - lambda表达式的“beta简化”
很简单地,当函数被调用时,函数中的形式参数会被替换成实际参数,比如F(2) = a*2+b,同理将lambda表达式(lambda (x)(+(* a x)b))应用于2,则其将被化简为(+ (* a 2) b) - lambda表达式的 "Currying”
不知道咋翻译这个操作,其表示的将lambda表达式的由m个参数的形式转化成具有n个参数的形式(其中m,n为正整数, m > n )
比如在我们的印象中,假设我们需要这样定义两个数的求和运算: (define (add a b) (+ a b)) , 这需要对add方法传入两个参数,比如 (add 2 3); 如果我们只允许add 带一个参数,那应该怎么办呢? 我们应该采用Currying这个技巧编写出下面的代码:
(define (add a)
(lambda (b) (+ a b)))
此后,调用add方法就只需要传入一个参数了,比如 (((add 2) 3)
同理,对于带有两个参数的lambda表达式 ((lambda (x y) (+ x y)) 2 3) 可以转换成由两个分别带一个参数的lambda表达式组合而成的组合表达式 ((lambda (x) ((lambda (y) (+ x y)) 3)) 2)。
之所以要这样做,其实在邱奇发明的“lambda 计算”中对lambda表达式的形式化定义中,lambda表达式本身就是只带一个参数的,要进行多个参数的lambda计算,currying则是必须的,当然,就普通程序员而言,我们仍然可以写出带有多个参数的lambda表达式而不必顾虑太多,因为解释器会帮我们做很多工作,但这不代表学习currying没有实际意义,一个明显的例证是在1.3.1节中的“练习1.33 ”,请参考“SICP学习笔记1.3.1”。 - lambda计数
如果问小孩子,1+2等于几?他可能会掰掰手指然后告诉你3。这里的“掰手指”是关键,在小孩看来这是一个计算过程,也是一个严格的证明过程。学了这么多年数学说,面对相同的问题,我们大概也很难说明为啥1+2就等于3,除了掰手指。
但邱奇却发明了一种计数方式,让你感觉轻松地推出1+2的确等于3
要理解邱奇数,得基于如下假设(下面的假设是我的个人理解,不知道有无数学依据,至少它可以帮助我们理解丘奇数):
如果满足下面的条件,我们就称发明了自然数记法
1)定义一个后继函数,它可以表述这样的含义“自然数要么是0,要么是自然数加1 ” 。
2)定义一套函数,它们分别能表述“加”,“减”,“乘”,“除”(或其它更多的操作)。
3)证明由后继函数产生的自然数能适用于这些操作。
假设数字n 我们用lambda表达式 (lambda (s z)(s^n z))表示,其中s^n 不是表示s的n次方,而是(s^n z)构成一个整体表示函数s在z上应用n次,(s^n z)等同于 (s(s (s …(s z))))一共n次。那么,
(lambda (s z)z) 表示 0
(lambda (s z)(s (s z)) 表示 1
(lambda (s z)(s (s(s z))) 表示 2
依次类推,与掰手指类似。
利用上述法则我们可以产生one,two,three这三个自然数:
(define one (lambda (s z) (s z)))
(define two (lambda (s z) (s(s z))))
(define three (lambda (s z) (s(s(s z)))))
现在假设加法操作add如下定义:
(define add (lambda (s z x y) (x s (y s z))))
我们来看看 (add one two)是如何得到three的:
;add 操作的定义,注意到这里是4个参数
(define add (lambda (s z x y) (x s (y s z))))
;利用Currying将4个参数减少到2个
(define add (lambda (x y) (lambda (s z) (x s (y s z)))))
;1和2相加
(add one two)
;在add操作的函数体中,利用belta转换,将x,y替换成one,two
(lambda (s z) (one s (two s z)))
;将one,two展开,利用的是alpha转换
(lambda (s z) ((lambda (s z) (s z)) s ((lambda (s z) (s(s z))) s z)))
;利用beta转换 ((lambda (s z) (s(s z))) s z) 实际上等同于 (s(s z))
;利用alpha转换代换,将((lambda (s z) (s(s z))) s z)代换成(s(s z))
(lambda (s z) ((lambda (s z) (s z)) s (s(s z))))
;利用beta转换 ((lambda (s z) (s z)) s (s(s z))) 实际上等同于 (s (s(s z)))
;利用alpha转换代换,将((lambda (s z) (s z)) s (s(s z)))代换成(s (s(s z)))
(lambda (s z) (s (s(s z))))
注意到(lambda (s z) (s (s(s z))))实际上就是three,也就是我们平时所说的3
通过这个简单的验证过程,我们开始感觉到“邱奇数”的美妙,不过有些遗憾的是我这里不能给出其严格的数学证明,也许以后可以。
2,let 和 lambda
对于表达式 ((lambda (x) (* (- x 1) 2) 5) 我们可以理解成“将一个lambda表达式应用于数字5”,那么在计算该表达式值时我们会利用beta化简将表达式中的形式参数替换成实际参数5,也就相当于在说“让形式参数具有值5,然后进行运行”,将这句话翻译成程序语言便是 (let( (x 5) ) (* (- x 1) 2) 。 可见这里的lambda表达式表达了与let相同的语义,所以我们说“let只不过是lambda的语法糖衣”。
作为一个简单的demo,你可以观察并运行下面的代码:
(define (F x) (let ((a (- x 1))) (* a 2)))
(define (G x) (* ((lambda (a) (- a 1)) x) 2))
(F 5)
(G 5)
它们将得到相同的结果
3,练习1.34
比较简单,运行下面的程序:
(define (square a) (* a a))
(define (f g) (g 2))
(f square)
(f (lambda (z) (* z (+ z 1))))
输出结果为 4 和 6
如果要对 (f f)求值的话,按照应用序展开:
(f f) => (f (f 2)) => (f (2 2)) 明显这里存在语法错误了:无法将前一个2作为函数来应用于后一个2
4,不动点
SICP上求零点的算法思想来自于折半查找,相对比较简单,这里略过,直接看看不动点(Fixed Point)。
A number x is called a fixed point of a function f if x satisfies the equation f(x) = x. For some functions f we can locate a fixed point by beginning with an initial guess and applying f repeatedly, f(x), f(f(x)) f(f(f(x)))…. until the value does not change very much.
通过这段话,我们明白以下几点:
- 不动点满足 f(x)=x ,也就是说它映射到自身。
- 它是函数曲线 y=f(x) 与 y=x 的交点,如果没有交点,那么函数f(x)也就没有不动点,比如 f(x)= x+2。
- 如果我们能将某个问题转化成 f(x)=x 的形式,那么对这个问题的求解实际上就是求 f(x)的不动点。
- 求不动点就是求递归函数 f(f(f…(f(x)))) 的值,递归的跳出条件是“当递归过程中值的变化率非常小”。
5,平均阻尼
不动点的求值过程总让人联想到高二物理课程的“阻尼振荡”,虽然不完全相同,但也有几分神似,我们知道在阻尼振荡中,随着能量被逐渐消耗,电流会逐渐变小,最后为0,如果,能量不断得到周期性的供给的话,其会在一个范围内不停振荡。同样的道理,在求不动点的过程中,我们希望随着递归次数的增加,值越来越接近我们的期望值,相反地,如果它始终徘徊在几个值之间的话,我们的递归函数将形成无穷递归。
比如,值一直在f(n)=1 和 f(n+1)=2 之间振荡的话,值的序列为1 2 1 2… 当我们将f(n+1)修正为f(n)与其值的平均值,那么值的序列将变成 1 1.5 1.25 1.125… 很明显,这个数列是收敛的。这个用平均值方法来修正f(n+1)值的方式,便是平均阻尼技术。
6,练习1.35
证明黄金分割率φ是 x –> 1+1/x 的不动点
上面在提到“不动点”的时候,我们说“如果我们能将某个问题转化成 f(x)=x 的形式,那么对这个问题的求解实际上就是求 f(x)的不动点”,那么此题就转化成:如何将黄金分割率转化成 f(x)=1+1/x 的形式。
由于黄金分割率满足方程 φ^2 = φ + 1
=> φ = (φ+1) / φ
=> φ = 1 + 1/φ
令 f(φ) = 1 + 1/φ
所以 φ = f(φ), 那么求满足φ = f(φ)的φ值实质就是求(φ) = 1 + 1/φ的不动点。
求黄金分割率:
(define (golden-mean x)
(fixed-point (lambda (x) (+ 1 (/ 1 x))) 2.0))
(golden-mean 8);这里x使用任何正数得到的结果是一样的
计算结果为 1.6180327868852458(求倒数便是 0.6180344478216819)
7,练习1.36
要证明x^x=1000的根式 x –> log(1000)/log(x) 非常简单,对x^x=1000方程两边同时取对数,然后依照练习1.35的方式就可以证明了。
具体的计算过程,参考下面的代码:
(define tolerance 0.00001)
(define (close-enough? v1 v2)
(< (abs (- v1 v2)) tolerance))
(define (average v1 v2)
(/ (+ v1 v2) 2))
(define (fixed-point f first-guess)
(define (try guess step-count)
(begin
(display "step")
(display step-count)
(display ":")
(display guess)
(newline)
(let ((next (f guess)))
(if (close-enough? guess next)
next
(try next (+ step-count 1))))))
(try first-guess 0))
(define (F x)
(fixed-point (lambda (x) (/ (log 1000) (log x))) 2.0))
(define (G x)
(fixed-point (lambda (x) (average x (/ (log 1000) (log x)))) 2.0))
(F 1000)
(G 1000)
step0:2.0
step1:9.965784284662087
step2:3.004472209841214
step3:6.279195757507157
step4:3.759850702401539
step5:5.215843784925895
step6:4.182207192401397
step7:4.8277650983445906
step8:4.387593384662677
step9:4.671250085763899
step10:4.481403616895052
step11:4.6053657460929
step12:4.5230849678718865
step13:4.577114682047341
step14:4.541382480151454
step15:4.564903245230833
step16:4.549372679303342
step17:4.559606491913287
step18:4.552853875788271
step19:4.557305529748263
step20:4.554369064436181
step21:4.556305311532999
step22:4.555028263573554
step23:4.555870396702851
step24:4.555315001192079
step25:4.5556812635433275
step26:4.555439715736846
step27:4.555599009998291
step28:4.555493957531389
step29:4.555563237292884
step30:4.555517548417651
step31:4.555547679306398
step32:4.555527808516254
step33:4.555540912917957
4.555532270803653
step0:2.0
step1:5.9828921423310435
step2:4.922168721308343
step3:4.628224318195455
step4:4.568346513136242
step5:4.5577305909237005
step6:4.555909809045131
step7:4.555599411610624
step8:4.5555465521473675
4.555537551999825
前者运算了34次,而后者仅运算了9次
8,练习1.37
利用“K项有穷连分式”求黄金分割率,比前面几个练习稍稍复杂一点,思维过程太难讲解了,自个看下面的代码慢慢体会吧:
(define (cont-frac n d k)
(cont-frac-iter n d k 0 (/ (n 1) (d 1))))
;cont-frac的迭代形式,i表示当前迭代次数,当它大于K时跳出迭代
;result 作为迭代结果的累积器,累积器的初始值也就是k为1时的值(/ (n 1) (d 1))
(define (cont-frac-iter n d k i result)
(if (> i k)
result
;先取得下一次的值next
(let ((next (cont-frac-iter n d k (+ i 1) result)))
;求本次的值
(cont-frac-iter n d k (+ i 1) (/ (n i) (+ (d i) next))))))
;k取3时能达到四位精度
(cont-frac (lambda (i) 1.0) (lambda (i) 1.0) 3)
运算结果:0.6180338134001252
9,练习1.38
利用“K项有穷连分式”求自然对数e
基于练习1.37的,不同的是d(i) 是关于i的数列:1 2 1 1 4 1 1 6 1 1 8….
那么关键在于写出能产生树立d(i)第 i 项的函数:
(define (mod x y) (floor (/ x y)))
(define (D i)
(if (= 0 (remainder (+ i 1) 3))
(* 2 (mod (+ i 1) 3))
1))
其中mod 求模,remainder 求余。
然后将D(i)代入到练习1.37中的cont-frac中便可。
10,练习1.39
和前面差不多的解法:
(define (cont-cf x k)
(/ x (cont-cf-iter x k 1 (- 1 (* x x)))))
(define (cont-cf-iter x k i result)
(if (> i k)
result
(let ((next (cont-cf-iter x k (+ i 1) result)))
(cont-cf-iter x k (+ i 1) (- (+ 1 (* i 2)) (/ (* x x) next))))))
注:这是一篇读书笔记,所以其中的内容仅属个人理解而不代表SICP的观点,并随着理解的深入其中的内容可能会被修改