SICP学习笔记(1.3.2 ~ 1.3.3)
                               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的观点,并随着理解的深入其中的内容可能会被修改 
 
                    
                     
                    
                 
                    
                 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号