• 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)

    其中,F没有采用平均阻尼技术,而G采用了,对比一下两者的运算结果:

    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的观点,并随着理解的深入其中的内容可能会被修改

  • 相关阅读:
    WPF画辐射图
    WPF 获取表格里面的内容
    C# 动态生成Html地图文件
    C#如何关闭指定进程
    oracle EM 打不开 503 |OracleDBConsoleorcl 启动不了
    oracle windows 下修复无监听错误-12541/12514
    Oracle 命令汇总
    oracle 函数 bitand 与 decode
    一.Git 初步扫盲
    修改字段类型
  • 原文地址:https://www.cnblogs.com/zhouyinhui/p/1583398.html
Copyright © 2020-2023  润新知