续延传递

续延传递(Continuation Passing Style, CPS)是一种编程手法,不要相信我可以将它讲清楚——在敲这些字的时候,我刚刚开始看《The Little Schemer》的第八章的 multirember&co 这个函数的定义,并且是由于看不懂,因此才写此文。编程

阶乘

下面是阶乘函数的定义:编程语言

(define (factorial n)
  (cond ((= n 1) 1)
        (else (* n (factorial (- n 1))))))

若是看不懂这个函数的定义,就不必再看下去了,应该先阅读 SICP 的第一章。函数式编程

下面是阶乘函数的另外一种形式的定义:函数

(define (factorial-cps n k)
  (cond ((= n 1) (k 1))
        (else (factorial-cps (- n 1) (lambda (v) (k (* v n)))))))

看不懂这个函数的定义,是正常现象,由于它是续延传递风格的函数。优化

不要理睬 factorial-cps 的定义,来看一下它的用法。下面的代码能够计算 3!:ui

> (factorial-cps 3 (lambda (z) z))
6

下面是 factorial-cps 在接受实参 3(lambda (z) z) 以后的执行过程:翻译

第一步code

(factorial-cps 2 (lambda (v) ((lambda (z) z) (* v 3))))

第二步:对象

(factorial-cps 1 (lambda (v') ((lambda (v) ((lambda (z) z) (* v 3))) (* v' 2))))

第三步递归

((lambda (v') ((lambda (v) ((lambda (z) z) (* v 3))) (* v' 2))) 1)

最后获得的这个表达式虽然复杂,但它只不过是让一个函数——三个逐层嵌套的匿名函数构成的函数做用于实参 1 而已。若是将这个表达式复制到 Guile 交互解释器中,求值结果为 6,刚好是 3!. 也就是说,这个表达式是真正的阶乘计算过程。factorial-cps 函数自己并未在阶乘方面进行任何计算,它所作的工做就是生成这个阶乘计算过程。

承诺链

虽然能够将 factorial-cps 的过程彻底展开,代码中的任何一个局部都可以理解,可是依然不理解代码的完整含义。由于咱们的思惟里尚未彻底的接纳续延的概念。

下面的代码

(factorial-cps 3 (lambda (z) z))

它做出了一个承诺:算出 3! 的结果后,我会将它传递给匿名函数 (lambda (z) z)。这个匿名函数只是简单的将参数做为返回值。

进入 factorial-cps 计算过程后,该函数会做出承诺:我要先计算 2!,而后将计算结果 v 交给一个匿名函数 (lambda (v) ((lambda (z) z) (* v 3))),由这个函数负责算出 3! 的结果。

factorial-cps 接受了实参 2(lambda (v) ((lambda (z) z) (* v 3))) 时,它承诺:我要先计算 1!,而后将计算结果 v' 交给匿名函数 (factorial-cps 1 (lambda (v') ((lambda (v) ((lambda (z) z) (* v 3))) (* v' 2)))),由这个函数负责算出 3! 的结果。

factorial-cps 传递接受实参 1(factorial-cps 1 (lambda (v') ((lambda (v) ((lambda (z) z) (* v 3))) (* v' 2)))) 时,factorial-cps 的第一个谓词为真,因而,就获得了:

((lambda (v') ((lambda (v) ((lambda (z) z) (* v 3))) (* v' 2))) 1)

这是一条承诺链,随着递归层次的增长,这条承诺链会愈来愈长。当递归达到终点时,承诺链便建好了。接下来,以重新到旧的顺序完成每一个承诺,即可获得 3! .

Fibonacci

下面是 Fibonacci 函数的定义:

(define (fib n)
  (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fib (- n 1)) (fib (- n 2))))))

若是看不懂这个函数的定义,就不必再看下去了,应该先阅读 SICP 的第一章。

下面是 Fibonacci 函数的续延传递版本:

(define (fib-cps n k)
  (cond ((= n 0) (k 0))
        ((= n 1) (k 1))
        (else (fib-cps (- n 1)
                   (lambda (v)
                     (fib-cps (- n 2)
                          (lambda (v')
                            (k (+ v v')))))))))

假设使用 fib-cps 计算 (fib n)

(fib-cps n (lambda (z) z))

这行代码做出了第一个承诺:我要算出 (fib n),将结果传给 (lambda (z) z)

对于 (fib-cps n (lambda (z) z)) 这个任务,fib-cps 函数做出承诺:我要先算出 (fib (- n 1)) 的结果 v,而后将 v 传递给下面这个匿名函数:

(lambda (v)
  (fib-cps (- n 2)
           (lambda (v')
             (k (+ v v')))))

这个匿名函数也是在做承诺:我先算出 (fib (- n 2)) 的值 v',而后将 v' 传给下面这个匿名函数:

(lambda (v')
  (k (+ v v')))

这个匿名函数也是在做承诺:我先算出 (+ v v'),而后将结果传给函数 k,而这里的 k 刚好是 (lambda (z) z)

续延传递

假设使用 fib-cps 计算 (fib n)

(fib-cps n (lambda (z) z))

(lambda (z) z)) 是一个续延,表示 fib-cps 函数在算出 (fib n) 的结果以后要作的事。这个几乎什么也没作的很是普通的匿名函数之因此能成为续延,是由于它接受的参数 zfib-cps 在应用这个匿名函数以前获得的计算结果——函数原本要做为结果返回的值变成了这个函数的续延的参数了。

将一个函数的续延做为参数传递给这个函数,这就是续延传递。

来看:

(lambda (v)
  (fib-cps (- n 2)
           (lambda (v')
             (k (+ v v')))))

这是 fib-cps 在计算出 (fib (- n 1)) 以后要作的事。这件事是什么呢?是 (k (+ (fib (- n 1)) (fib (- n 2)))),而 k 就是 fib-cps 在计算出 (fib n) 以后要作的事,即 (lambda (z) z)

有什么用?

将本来很直白的

(define (fib n)
  (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fib (- n 1)) (fib (- n 2))))))

写成

(define (fib-cps n k)
  (cond ((= n 0) (k 0))
        ((= n 1) (k 1))
        (else (fib-cps (- n 1)
                   (lambda (v)
                     (fib-cps (- n 2)
                          (lambda (v')
                            (k (+ v v')))))))))

这样作有什么好处?

注意 fibfib-cps 的递归形式的不一样,前者是符合人类思惟模式的普通递归,后者是尾递归——函数的求值结果是其自身的应用。尾递归的好处是,编译/解释器有机会将其优化为不会致使栈溢出的形式。

非尾递归形式的递归函数,由于上层递归过程在下层递归过程返回计算结果以前不会退出,因此它们占据的栈空间不会被释放。这样,每执行一次递归都会消耗一部分栈空间——当递归深度过大时,栈空间会耗尽,致使运算过程当中断。若是将这种递归形式改写为续延传递形式,那么它就变成尾递归了。因为尾递归函数,每次递归时,其上层递归过程已经彻底执行完了,它们不必再占据栈空间,所以编译器/解释器能够将它们所占用的栈空间释放——尾递归优化。

王垠写了 40 行咱们看不懂的 Scheme 代码。听说这 40 行代码能够自动将非尾递归形式的递归函数『翻译』为续延传递风格的函数。若是真的是这样,这彷佛是很是强大的技术。有了这种技术,不再用担忧递归会致使栈溢出。不过,fib-cps 虽然是尾递归的,但其运算效率可能还不及 fib!下面的 fib* 是效率更高的尾递归形式:

(define (fib* n)
  (define (fib-iter a b count)
    (cond ((= count 0) b)
          (else (fib-iter (+ a b) a (- count 1)))))
  (fib-iter 1 0 n))

这种尾递归形式可能不可能存在相似王垠 40 行代码的代码自动变换出来,它是基于动态规划方法构造出来的。基于动态规划方法所构造的运算过程一般与具体的问题密切相关。

事实上,fib-cps 只是从机器层面消除了递归栈,但逻辑上的递归栈依然是存在的,这个栈就是做为函数参数的续延,它会随着递归的深度不断的累积。从机器角度来看,续延传递变换能够将递归过程所用的栈空间转换为存储递归过程的堆空间——前提是编译器/解释器不会将续延对象复制到栈空间再传递给函数,而是传递续延对象的引用。

也就是说,虽然基于续延传递变换,能够将任何递归函数变化为尾递归形式,可是这并不是续延传递变换真正重要的应用场景。甚至能够说续延传递变换与尾递归没有必然联系,只不过续延传递变换刚好在形式上是尾递归而已。

那么续延传递变换真正重要的应用场景是什么?经过阶乘与 Fibonacci 函数的例子能够看出,续延传递风格的函数能够将特定的计算过程在递归函数的参数中积累起来。换句话说,基于续延传递,能够将特定的递归运算过程展开为一个层层嵌套的函数构成的表达式,就像在作泰勒展开运算同样,使得编译器/解释器有机会对展开结果进行优化。不过,我不清楚具体能够优化什么,这是函数式编程语言编译/解释器专家关心的事。

结语

写完此文后,我依然没看完《The Little Schemer》的第八章。

相关文章
相关标签/搜索