Scheme语言直译为汉语(八)

用高阶函数做抽象

在作用上,过程也就是一类抽象,它们描述了一些对于数的复合操作,但又不依赖于特定的数。例如,在定义:

(define (cube x) (* x x x))

(尝试把上面这句直译为中文):

(定义 (立方 元) (* 元 元 元))

时,我们讨论的并不是某个特定数值的立方,而是对任意的数得到其立方的方法。
除了把数值计算抽象为过程并把数值作为参数,我们还可以针对高阶计算过程把一些简单的数值计算过程本身作为参数来进行进一步的抽象。

一、过程作为参数

考虑下面三个过程,第一个计算从a到b的各整数之和:

(define (sum-integers a b)
    (if (> a b)
        0
        (+ a (sum-integers (+ a 1) b))))

尝试把上述过程直译为中文(计算从甲到乙的各整数之和):

(定义 (整数求和 甲 乙)
    (如果 (> 甲 乙)
        0
        (+ 甲 (整数求和 (+ 甲 1) 乙))))

第二个计算给定范围内整数的立方之和:

(define (sum-cubes a b)
    (if (> a b)
        0
        (+ (cube a) (sum-cubes (+ a 1) b))))

尝试把上述过程直译为中文:

(定义 (立方求和 甲 乙)
    (如果 (> 甲 乙)
        0
        (+ (立方 甲) (立方求和 (+ 甲 1) 乙))))

第三个计算下面的序列之和:

1 1 ⋅ 3 + 1 5 ⋅ 7 + 1 9 ⋅ 11 + … … \frac{1}{1 \cdot 3}+\frac{1}{5 \cdot 7}+\frac{1}{9 \cdot 11}+…… 131+571+9111+
它将(非常缓慢地)收敛到 π \pi π/8:

(define (pi-sum a b)
    (if (> a b)
        0
        (+ (/ 1.0 (* a (+ a 2))) (pi-sum (+ a 4) b))))

尝试把上述过程直译为中文:

(定义 (八分之派求和 甲 乙)
    (如果  (> 甲 乙)
        0
        (+ (/ 1.0 (* 甲 (+ 甲 2))) (八分之派求和 (+ 甲 4) 乙))))

可以明显看出,这三个过程共享着一种公共的基础模式。我们可以通过填充下面模板中的各空位,产生出上面的各个过程:

(define (<name> a b)
    (if (> a b)
        0
        (+ (<term> a) 
           (<name> (<next> a) b))))

尝试把上述模板译为中文:

(定义 (<求和过程> 甲 乙)
    (如果 (> 甲 乙)
        0
        (+ (<操作> 甲) 
           (<求和过程> (<下一个> 甲) 乙))))

当然,数学家们很早就认识到序列求和中的抽象模式,并提出了专门的“求和记法”,例如:
∑ n = a b f ( n ) = f ( a ) + … … + f ( b ) \sum_{n=a}^{b}f(n) = f(a) + …… + f(b) n=abf(n)=f(a)++f(b)
哈哈,我想这么来翻:
∑ n = 甲 乙 道 ( 元 ) = 道 ( 甲 ) + … … + 道 ( 乙 ) \sum_{n=甲}^{乙}道(元) = 道(甲) + …… + 道(乙) n=()=()++()
那么我们希望我们的语言也足够强大,能够写出一个过程去表示更为宏观的求和的概念,而不仅仅只能写计算特定求和的过程。那要做到这一点,其实只要按照上面给出的模式,将其中的“空位”翻译为形式参数:

(define (sum term a next b)
    (if (> a b)
        0
        (+ (term a) 
           (sum term (next a) next b))))

尝试把上述过程直译为中文:

(定义 (求和过程 "形式参数":操作 甲 “形式参数”:下一个 乙)
    (如果 (> 甲 乙)
        0
        (+ (操作 甲) 
           (求和过程 操作 (下一个 甲) 下一个 乙))))

再尝试用上述过程重新定义sum-cubes(还需要一个过程inc,它得到参数值加一):

(define (inc n)(+ n 1))
(define (sum-cubes a b)
    (sum cube a inc b))

尝试把上述过程直译为汉语:

(定义 (自增 元)(+ 元 1))
(定义 (立方求和 甲 乙)
    (求和过程 立方 甲 自增 乙))

可以用这个过程算出从1到10的立方和:

(sum-cubes 1 10)
3025

尝试把上句直译为汉语:

(立方求和 1 10)
3025

利用一个恒等函数帮助算出项值,我们就可以基于sum定义出sum-integers

(define (identity x) x)
(define (sum-integers a b)
    (sum identity a inc b))

尝试把上述过程直译为汉语:

(定义 (值 元) 元)
(定义 (整数求和 甲 乙)
    (求和过程 值 甲 自增 乙))

而后就可以求出1到10的整数之和了:

(sum-integers 1 10)
55

尝试把上句直译为汉语:

(整数求和 1 10)
55

我们也可以用同样的方式定义pi-sum:

(define (pi-sum a b)
    (define (pi-term x)
        (/ 1.0 (* x (+ x 2))))
    (define (pi-next x)
        (+ x 4))
    (sum pi-term a pi-next b))

尝试把上述过程直译为汉语:

(定义 (八分之派求和 甲 乙)
    (定义 (八分之派求和公式中的操作 元)
        (/ 1.0 (* 元 (+ 元 2))))
    (定义 (八分之派求和公式中的下一个 元)
        (+ 元 4))
    (求和过程 八分之派求和公式中的操作 甲 八分之派求和公式中的下一个 乙))

注:可以想到,像pi-next,pi-term这些函数的定义不大可能用于其它地方,我们之后会聊如何摆脱这种定义。

一旦有了sum,我们就能用它作为基本构件,去形式化其它概念。例如,求出函数f在范围a和b之间的定积分的近似值,可以用下面公式完成:

∫ a b f = [ f ( a + d x 2 ) + f ( a + d x + d x 2 ) + f ( a + 2 d x + d x 2 ) + … … ] d x \int_a^bf=[f(a+\frac{dx}{2}) + f(a+dx+\frac{dx}{2}) + f(a+2dx+\frac{dx}{2}) + ……]dx abf=[f(a+2dx)+f(a+dx+2dx)+f(a+2dx+2dx)+]dx
嘿嘿,我觉着这样写也无大碍:
∫ 甲 乙 道 = [ 道 ( 甲 + 微 元 2 ) + 道 ( 甲 + 微 元 + 微 元 2 ) + 道 ( 甲 + 2 微 元 + 微 元 2 ) + … … ] 微 元 \int_甲^乙道=[道(甲+\frac{微元}{2}) + 道(甲+微元+\frac{微元}{2}) + 道(甲+2微元+\frac{微元}{2}) + ……]微元 =[(+2)+(++2)+(+2+2)+]
其中的dx(微元)是一个很小的值。我们可以将这个公式直接描述为一个过程:

(define (integral f a b dx)
    (define (add-dx x)(+ x dx))
    (* (sum f (+ a (/ dx 2.0)) add-dx b)
        dx))

尝试把上述过程直译为汉语:

(定义 (积分 道 甲 乙 微元)
    (定义 (加微元 元)(+ 元 微元))
    (* (求和过程 道 (+ 甲 (/ 微元 2.0)) 加微元 乙)
        微元))
(integral cube 0 1 0.01)
.24998750000000042
(integral cube 0 1 0.001)
.249999875000001

尝试把上面两句直译为汉语:

(积分 立方 0 1 0.01)
.24998750000000042
(积分 立方 0 1 0.001)
.249999875000001

(cube(立方式)在0和1之间积分的精确值是1/4。)

二、用lambda构造过程

在上文中的使用sum过程中,我们必须定义出一些如pi-term和pi-term一类的简单函数,以便用它们作为高阶函数的参数,这种做法看起来很不舒服。如果不需要显示定义pi-term和pi-next,而是有一种方法去直接刻画“那个返回其输入值加4的过程”和“那个返回其输入与它加2的乘积的倒数的过程”,事情就会方便多了。我们可以通过引入一种lambda特殊形式完成这类描述,这种特殊形式能够创建出所需要的过程。利用lambda,我们就能按照如下方式写出所需的东西:

(lambda (x)(+ x 4))

(lambda (x)(/ 1.0 (* x (+ x 2))))

尝试把上面两句直译为中文:

(规定 (元)(+ 元 4))

(规定 (元)(/ 1.0 (* 元 (+ 元 2))))

这样就可以直接描述pi-sum过程,而无须定义任何辅助过程了:

(define (pi-sum a b)
    (sum (lambda (x) (/ 1.0 (* x (+ x 2))))
         a
         (lambda (x) (+ x 4))
         b))

尝试把上述过程直译为中文:

(定义 (派求和 甲 乙)
    (求和过程 (规定 (元) (/ 1.0 (* 元 (+ 元 2))))
             甲
             (规定 (元) (+ 元 4))
             乙))

借助于lambda,我们也可以写出integral过程而不需要定义辅助过程add-dx:

(define (integral f a b dx)
    (* (sum f
            (+ a (/ dx 2.0))
            (lambda (x) (+ x dx))
            b)
        dx))

尝试把上述过程直译为中文:

(定义 (积分 道 甲 乙 微元)
    (* (求和过程 道
                (+ 甲 (/ 微元 2.0))
                (规定 (元) (+ 元 微元))
                乙)
        微元))

一般而言,lambda用与define同样的方式创建过程,除了不为有关过程提供名字之外:

(lambda (<formal-parameters>) <body>)

这样得到的过程与通过define创建的过程完全一样, 仅有的不同之处,就是这种过程没有与环境中的任何名字相关联。事实上,

(define (plus4 x) (+ x 4))

尝试把上面这句直译为中文:

(定义 (加四 元) (+ 元 4))

等价于

(define plus4 (lambda (x) (+ x 4)))

尝试把上面这句直译为中文:

(定义 加4(规定 (元) (+ 元 4)))

三、用let创建局部变量

编程时,我们常常需要用到局部变量,比如,假定我们希望计算函数:
f ( x , y ) = x ( 1 + x y ) 2 + y ( 1 − y ) + ( 1 + x y ) ( 1 − y ) f(x, y) = x(1 + xy)^2 + y(1 - y) + (1 + xy)(1 - y) f(x,y)=x(1+xy)2+y(1y)+(1+xy)(1y)
可能就希望将它表述为:
a = 1 + x y b = 1 − y f ( x , y ) = x a 2 + y b + a b \begin{aligned} a &= 1 + xy \\ b &= 1-y \\ f(x, y) &= xa^2 + yb + ab \end{aligned} abf(x,y)=1+xy=1y=xa2+yb+ab

嗯……:
道 ( 线 , 面 ) = 线 ( 1 + 线 ⋅ 面 ) 2 + 面 ( 1 − 面 ) + ( 1 + 线 ⋅ 面 ) ( 1 − 面 ) 道(线, 面) = 线(1 + 线\cdot面)^2 + 面(1 - 面) + (1 + 线\cdot面)(1 - 面) (线,)=线(1+线)2+(1)+(1+线)(1)
甲 = 1 + 线 ⋅ 面 乙 = 1 − 面 道 ( 线 , 面 ) = 线 ⋅ 甲 2 + 面 ⋅ 乙 + 甲 ⋅ 乙 \begin{aligned} 甲 &= 1 + 线\cdot面 \\ 乙 &= 1-面 \\ 道(线, 面) &= 线\cdot甲^2 + 面\cdot乙 + 甲\cdot乙 \end{aligned} (线,)=1+线=1=线2++

在写计算f的过程时,我们可能希望还有几个局部变量,不止是x和y,还有中间值的名字如a和b。做到这些的一种方式就是利用辅助过程去约束局部变量:

(define (f x y)
    (define (f-helper a b)
        (+ (* x (square a))
           (* y b)
           (* a b)))
        (f-helper (+ 1 (* x y))
                  (- 1 y)))

尝试把上述过程直译为汉语:

(定义 (道 线 面)
    (定义 (换元法求道 甲 乙)
        (+ (* 线 (平方 甲))
           (* 面 乙)
           (* 甲 乙)))
        (换元法求道 (+ 1 (* 线 面))
                  (- 1 面)))

对于上面这个过程,可以使用let将之写得可读性更强些:

(define (f x y)
    (let ((a (+ 1 (* x y)))
          (b (- 1 y)))
      (+ (* x (square a))
         (* y b)
         (* a b))))

尝试将上述过程直译为汉语:

(定义 (道 线 面)
    (命 ((甲 (+ 1 (* 线 面)))
         (乙 (- 1 面)))
      (+ (* 线 (平方 甲))
         (* 面 乙)
         (* 甲 乙))))

四、过程作为一般性的方法

1. 二分法查找方程的根

假定开始时给定函数f和使函数值取值为负和正的两个点。首先算出两个给定点的中点,而后检查给定区间是否足够小。如果是的话,就返回这一中点的值作为回答;否则就算出 f f f在这个中点的值。如果检查发现 f f f在这个中点的值为正,那么就从原来值为负的点到中点的新区间继续二分查找;如果 f f f在这个中点的值为负,则就从中点到原来值为正的点的新区间进行二分查找。也存在着检测值恰好为0的可能性,这时中点就是我们要寻找的根:

(define (search f neg-point pos-point)
    (let ((midpoint (average neg-point pos-point)))
        (if (close-enough? neg-point pos-point)
            midpoint
            (let ((test-value (f midpoint)))
                (cond ((positive? test-value)
                       (search f neg-point midpoint))
                      ((negative? test-value)
                       (search f midpoint pos-ponit))
                (else midpoint))))))

尝试把上述过程直译为汉语:

(定义 (二分查找 道 负值点 正值点)
    (命 ((中点 (求平均 负值点 正值点)))
        (如果 (靠得足够近? 负值点 正值点)
            中点
            (命 ((中点处道值 (道 中点)))
                (情况符合 ((为正? 中点处道值)
                          (二分查找 道 负值点 中点))
                         ((为负? 中点处道值)
                          (二分查找 道 中点 正值点))
                (其它情况 中点))))))

还差一个判断是否“靠得足够近”的过程没有实现:

(define (close-enough? x y)
    (< (abs (- x y)) 0.01))

尝试把上面这句直译为汉语:

(定义 (靠得足够近? 甲 乙)
    (< (绝对值 (- 甲 乙)) 0.01))

上述算法要使用的话还有一个前提,就是要分别有一个令函数取值为负的点和一个令函数取值为正的点被传进来,那凭啥就能确保一个点能令函数取值为正,一个点令函数取值为负?万一算错了呢?所以为了程序健壮性考虑,要在外面再套一层,判断只有当两个点符合一个会使得函数取值为正,另一个使得函数取值为负,才会执行上述过程,否则抛出错误信息:

(define (half-interval-method f a b)
    (let ((a-value (f a))
          (b-value (f b)))
    (cond ((and (negative? a-value) (positive? b-value))
           (search f a b))
          ((and (negative? b-value) (positive? a-value))
           (search f b a))
          (else
           (error "Values are not of opposite sign" a b)))))

尝试把上述过程直译为汉语:

(定义 (使用二分查找 道 甲 乙)
    (命 ((甲处道值 (道 甲))
         (乙处道值 (道 乙)))
    (情况符合 ((&& (为负? 甲处道值) (为正? 乙处道值))
              (二分查找 道 甲 乙))
             ((&& (为负? 乙处道值) (为正? 甲处道值))
              (二分查找 道 乙 甲))
             (否则
              (错误 "两处道值非一正一负" 甲 乙)))))

2.找出函数道不动点
如果 x x x满足 f ( x ) = x f(x) = x f(x)=x,则数 x x x称为函数 f f f的不动点。对于某些函数,通过从某个初始猜测出发,反复地应用 f f f

f ( x ) , f ( f ( x ) ) , f ( f ( f ( x ) ) ) , . . . f(x),f(f(x)),f(f(f(x))),... f(x),f(f(x)),f(f(f(x))),...
直到值的变化不大时,就可以找到它的一个不动点。根据这个思路,我们可以设计出一个过程fixed-point,它以一个函数和一个初始猜测为参数,产生出该函数的一个不动点的近似值。我们将反复应用这个函数,直至发现连续的两个值之差小于某个事先给定的容许值:

(define tolerance 0.00001)

(define (fixed-point f first-guess)
    (define (close-enough? v1 v2)
        (< (abs (- v1 v2)) tolerance))
    (define (try guese)
        (let ((next (f guess)))
            (if (close-enough? guess next)
                next
                (try next))))
    (try first-guess))

尝试把上述过程直译为汉语:

(定义 可忍受的精确度 0.00001)

(定义 (反复求道计算不动点 道 初始猜测点)
    (定义 (值足够接近? 甲 乙)
        (< (绝对值 (- 甲 乙)) 可忍受的精确度))
    (定义 (尝试 猜测点)
        (命 ((猜测点处道值 (道 猜测点)))
            (如果 (值足够接近? 猜测点 猜测点处道值)
                猜测点处道值
                (尝试 猜测点处道值))))
    (尝试 初始猜测点))

(定义 (不动点 道 初始猜测点)
    (反复求道计算不动点 道 初始猜测点))

例如,下面用这一方法求出点是余弦函数的不动点,其中用1作为初始近似值:

(fixed-point cos 1.0)

尝试把上面这句直译为汉语:

(不动点 余弦道 1.0)

在这里插入图片描述

类似地,我们也可以找出方程 y = s i n y + c o s y y=sin y +cos y y=siny+cosy的一个解:

(fixed-point (lambda (y) (+ (sin y) (cos y)))
             1.0)

尝试把上面这句直译为汉语:

(不动点 (规定 (线) (+ (正弦道 线) (余弦道 线)))
             1.0)

在这里插入图片描述

这一不动点的计算过程使人回忆起先前找平方根的计算过程。两者都是基于同样的想法:通过不断地改进猜测,直至结果满足某一评价准则为止。事实上,我们完全可以将平方根的计算形式化为一个寻找不动点的计算过程。计算某个数 x x x的平方根,就是要找到一个 y y y使得 y 2 = x y^2=x y2=x。将这一等式变成另一个等价形式 y = x / y y =x/y y=x/y,就可以发现,这里要做的就是寻找函数 y ↦ x / y y \mapsto x/y yx/y的不动点。因此,可以用下面方式试着去计算平方根:

(define (sqrt x)
    (fixed-point (lambda (y) (/ x y))
                 1.0))

尝试把上述过程直译为汉语:

(定义 (开方 线)
    (不动点 (规定 (面) (/ 线 面))
                     1.0))

遗憾的是,这一不动点搜寻并不收敛。考虑某个初始猜测 y 1 y1 y1,下一个猜测将是 y 2 = x / y 1 y_{2} = x/y_{1} y2=x/y1,而再下一个猜测是 y 3 = x / y 2 = x / ( x / y ) = y 1 y_{3} =x/y_{2}= x/(x/y)=y_{1} y3=x/y2=x/(x/y)=y1。结果是进入了一个无限循环,其中没完没了地反复出现两个猜测 y 1 y_{1} y1 y 2 y_{2} y2,在答案的两边往复振荡。

控制这类振荡的一种方法是不让有关的猜测变化太剧烈。因为实际答案总是在两个猜测 y y y x / y x/y x/y之间,我们可以做出一个猜测,使之不像 x / y x/y x/y那样远离 y y y,为此可以用y和/y的平均值。这样,我们就取 y y y之后的下一个猜测值为 ( 1 / 2 ) ( y + x / y ) (1/2)(y +x/y) (1/2)(y+x/y)而不是 x / y x/y x/y。做出这种猜测序列的计算过程也就是搜寻 y ↦ ( 1 / 2 ) ( y + x / y ) y \mapsto (1/2)(y +x/y) y(1/2)(y+x/y)的不动点:

(define (sqrt x)
    (fixed-point (lambda (y) (average y (/ x y)))
                 1.0))

尝试把上述过程直译为汉语:

(定义 (开根 线)
    (不动点 (规定 (面) (求平均 面 (/ 线 面)))
                 1.0))

(请注意, y = ( 1 / 2 ) ( y + x / y ) y=(1/2)(y+x/y) y=(1/2)(y+x/y)是方程 y = x / y y=x/y y=x/y经过简单变换的结果,导出它的方式是在方程两边都加 y y y,然后将两边都除以2。)

经过这一修改,平方根过程就能正常工作了。事实上,如果我们仔细分析这一定义,那么就可以看到,它在求平方根时产生的近似值序列,正好就是之前的文章中的另一个求平方根过程产生的序列。这种取逼进一个解的一系列值的平均值的方法,是一种称为平均阻尼的技术,它常常用在不动点搜寻中,作为帮助收敛的手段。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X-jazz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值