scheme心得(3) 尾调用/尾递归与CPS

来源:互联网 发布:nginx阿里云绑定域名 编辑:程序博客网 时间:2024/06/09 16:13

摘要:介绍了尾调用/尾递归调用(tail call/tail recursive call)的一般概念、形式、用法,以及CPS(continuation-passing style)的编程模式。

  函数式编程非常重要的一个概念是递归。在函数式编程里面,递归是原生的合乎情理的,是从lambda计算继承而来的;而迭代(循环)是不良的,因为它的副作用,因为迭代(循环)意味着修改变量的值,违背了函数式编程的本意。修改变量的值是某些纯粹的函数式编程语言所坚决摈弃的,比如haskell。而scheme采取了更务实的做法,支持修改变量的值,并且保留了迭代(循环)的语法。这是非常有趣和值得探讨的。

  我们知道,当一个函数的返回语句是另一个函数的直接调用,可称为尾调用tail call。尾调用的优点是节省程序运行时间和空间。因为函数每一层的嵌套调用,都意味着一个新的栈帧stack frame。栈帧保留了诸如实参值,局部变量,访问链,返回地址等信息,构造和销毁栈帧是程序运行时的开销。而尾调用,可以避免在内存中保留caller函数的栈帧,只需要直接把caller的返回地址复制到callee的返回地址。一般情况下的尾调用,优势不明显,但是如果是层次非常多的递归调用,则可以大大发挥作用。这种递归叫做尾递归。优化后的尾递归,即直接传递返回地址的尾递归,与过程式编程的迭代(循环)类同。换句话说,也可以编译为goto或者jump语句。

  以阶乘函数为例。

(define (factorial n)  (if (= n 0) 1      (* n (factorial (- n 1)))))
这是非尾递归的阶乘。可以看出,每次递归调用,都要保留caller函数的栈帧,因为callee的返回值还要继续*n的操作才能返回caller的值。

(define (factorial n acc)  (if (= n 0) acc      (factorial (- n 1) acc*n)))
这是尾递归的阶乘。其中factorial函数额外有一个参数来保存累积的值。把累积的值传递到下次递归,递归函数返回值可以直接返回到最开始调用的位置。这样阶乘函数变成了尾递归的形式。

  因为尾递归在编译器层面可以优化为迭代,所以scheme除了显式的定义尾递归函数,还有两种独立的语法形式,用于尾递归或迭代。一种是let proc ((var1 initial1)...) exp,(var1 initial1)...表示初始化各参数,exp中尾递归调用proc var1 ...;另一种是do ((var1 initial1 step1)...) exp,(var1 initial1 step1)...表示初始化各变量并定义迭代步长,exp中操作各变量。

(let loop     ((numbers '(3 -2 1 6 -5))      (noneg '())      (neg '()))   (cond ((null? numbers)          (list nonneg neg))         ((>=(car numbers) 0)          (loop (cdr numbers)                (cons (car numbers) nonneg)                neg))         (else (loop (cdr numbers)                     nonneg                     (cons (car numbers) neg)))));((6 1 3)(-5 -2))

以上是named let的用法。它初始化numbers、noneg、neg变量后,依次将numbers的头元素取出,将非负值放入noneg,负值放入neg。

(let ((x '(1 3 5 7 9)))  (do ((x x (cdr x))       (sum 0 (+ sum (car x))))      ((null? x) sum)));25

以上是do的用法。它初始化x、sum并定义迭代步长,将x的头元素取出累加。最后得到25。

  有一种特殊形式是CPS,continuation-passing style。CPS的含义是函数的参数包括一个显式的continuation,以及一个tail call尾调用,前面的continuation传递给尾调用的函数作为其参数。CPS同样可以进行尾调用优化。continuation的定义和用法在前文中有介绍。

  CPS在一般编程中使用不多,更多是编译器里面使用到。每个函数都可以转换为PCS形式,这叫做PCS transformation。举个简单的例子,函数(+ x y)转换CPS形式则变成:

(define (+& x y k)  (k (+ x y)))
k是continuation,如果结合call/cc,有下面的例子,其中continuation是取出值并打印出来。

(define (+/k x y)  (display     (call/cc (lambda(k)(+& x y k)))))
对于一般的函数f,转换为PCS形式则可以定义为:

(define (cps-prim f)  (lambda args    (let ((r (reverse args)))      ((car r) (apply f                 (reverse (cdr r)))))))

(cps-prim f)返回一个函数,其最后一个参数是continuation,其余参数送到原函数f中求f的值。举例如下:

(define *& (cps-prim *))(define +& (cps-prim +))
(cps-prim *)和(cps-prim +)得到的都是PCS形式的*和+运算。

  最后举一个例子。仍然是阶乘函数。尾递归的阶乘函数如下:

(define (factorial n)  (f-aux n 1))(define (f-aux n a)  (if (= n 0)      a      (f-aux (- n 1)(* n a))))
而CPS形式的阶乘函数如下:
(define (factorial& n k)  (=& n 0 (lambda(b)            (if b                (k 1)                (-& n 1 (lambda(nm1)                          (factorial& nm1 (lambda(f)                                            (*& n f k)))))))))
(define (factorial& n k) (f-aux& n 1 k))(define (f-aux& n a k)  (=& n 0 (lambda(b)            (if b                 (k a)                (-& n 1 (lambda(nm1)                          (*& n a (lambda(nta)                                    (f-aux& nm1 nta k)))))))))
两个函数中,所有的函数和运算符都要先转换为CPS形式。第一个函数中,每次调用CPS形式的子函数,上一层的continuation传递进去经过包装,作为下一层嵌套的子函数的continuation参数。例如=&函数的continuation是lambda(b)...,(= n 0)的bool值作为参数b,计算lambda(b)...的值。(- n 1)的值作为nm1代入lambda(nm1)...计算factorial& nm1 (lambda(f)...),其中对新的factorial&,lambda(f)(*& n f k)作为新的continuation k。这个函数虽然是CPS形式,但是因为continuation中记录了层层嵌套的复杂的计算过程,并不能尾递归优化。第二个函数可以实现尾递归优化,因为用到了辅助的f-aux&函数。尾递归过程实际上就是(if (= n 0) (k a) (f-aux& (- n 1) (* n a) k)),如果将=&、-&、*&三个函数转为普通形式,则阶乘函数的代码如下:
(define (factorial& n k) (f-aux& n 1 k))(define (f-aux& n a k)  (if (= n 0) (k a)              (f-aux& (- n 1) (* n a) k)))
对比前面的(factorial n)函数,无非就是多了个continuation参数而已。这就是CPS。

0 0
原创粉丝点击