Continuation与CPS(Continuation Passing Style)的理解
程序员文章站
2022-03-10 21:02:03
...
Scheme是最早支持Continuation的语言,而Continuation对于初学者来说还是比较难以理解的,以下是我在学习TSPL中Continuation和CPS相关章节时的一些理解。更多Scheme、Continuation、CPS的相关知识到http://www.scheme.com/tspl4/ 查找学习。
* Continuation
一个表达式的continuation就是外部函数要利用该表达式产生的结果做什么。 比如:
(cons (exp) 'a)这里面(exp)的continuation就是等待exp的结果然后将其cons到'a上,它保存 了等待(exp)结果的计算点并进行后续计算的函数调用栈! 在scheme中,所有的continuation都可以用call/cc(call with current continuation来捕捉到(call/cc必须接受一个单一参数的函数作为参数):
(call/cc (lambda (k) exp))这里面k就是捕捉到的continuation。 每当显式的调用k的时候,我们就可以简单的把它想成直接将 (call/cc…)替换成传递给k的值并继续执行。比如call/cc放在递归的终止条件处,那么 每次调用continuation的时候就会将终止返回值替换成传递给coninuation的 值,再或者局部退出的例子(下面程序),将call/cc放到内部函数的最外部,调用 continuation的时候就直接结束程序了。 下面是利用call/cc来写的product函数:
(define product (lambda (ls) ;显式调用break的时候,直接跳到这里,也就是返回(call/cc。。。)的返 回值 (call/cc (lambda (break) (let f ([ls ls]) (cond [(null? ls) 1] [(= (car ls) 0) (break "zero")] [else (* (car ls) (f (cdr ls)))])))))) ;test (product '(1 2 3 4 5)) => 120 (product '(7 3 8 0 1 9 5)) => "zero"关于那个阴阳谜题,如果用上面这种代换的方式来考虑的话,似乎就没有那么难理解 了。
* CPS
- 正常函数的返回都隐含一个continuation,就是利用这个函数的返回值来 做的后续事情,而cps的本质就是将这个隐式的continuation显式的当做参 数传递进去,并在函数中完成应有的continuation并将最终结果返回。
- 这跟尾递归似乎很像,在改造递归为尾递归的时候,就将当前状态通过accumulator汇集到函数内部的操作,当达到结束条件时返回汇集结果,而不必再返回来收集递归过程中的返回值。
- cps似乎就是同样的道理,每次将continuation传递到内部进行操作的组合,当达到底部的时候直接将汇集的continuation的计算结果返回,而不必返回来再去计算每一步的continuation。
** TSPL的练习 3.4.3
Rewrite the following expression in CPS to avoid using call/cc.
(define reciprocals (lambda (ls) (call/cc (lambda (k) (map (lambda (x) (if (= x 0) (k "zero found") (/ 1 x))) ls))))) (reciprocals '(2 1/3 5 1/4)) (1/2 3 1/5 4) (reciprocals '(2 1/3 0 5 1/4)) "zero found"这道题是要用CPS的形式重写上面的函数,就是实现局部退出的功能。如果不规 定用map函数来写的话,我们可以按照之前product的CPS形式的写法来写
(define reciprocals (lambda (ls k) (let ([break k]) (let f ([ls ls] [k k]) (cond [(null? ls) (k '())] [(= (car ls) 0) (break "zero!")] [else (f (cdr ls) (lambda (x) (k (cons (/ 1 (car ls)) x))))])))))但如果不改变原来实现的方式,也就是用map函数和CPS来实现的话就会相对麻烦, 如果对CPS和Continuation理解不透彻的话会非常别扭,而且难以理解。Kent在 书中也给了提示:让我们看一下map1的实现,也就是说我们要实现一个可以传递 continuation的map,然后在转换函数中选择不同的continuation来完成异常情 况的检查与退出。 先看一下map1的实现:
(define map1 (lambda (p ls) (if (null? ls) '() (cons (p (car ls)) (map1 p (cdr ls))))))在这里让人比较迷惑的是在非尾部调用的过程中,cons会等待p 和 map1两个函数的结果 来进行后续的计算,也就是存在两个延续,那要怎么安排才好呢?我们看一下 tspl中的原话:
When one procedure invokes another via a nontail call, the called procedure receives an implicit continuation that is responsible for completing what is left of the calling procedure's body plus returning to the calling procedure's continuation. If the call is a tail call, the called procedure simply receives the continuation of the calling procedure.也就是说,非尾部调用的时候,被调用函数会隐式的接收到一个continuation, 这个continuation代表着"完成调用函数其他部分加上被调用函数的返回值". 如 果我们按照从右往左的计算顺序,那么我们可以对map1进行第一步改造:将隐式传递 给内部map1的continuation显式的写出来:
(define map1 (lambda (p ls k) (if (null? ls) (k '()) (map1 p (cdr ls) (lambda (v) (cons (p (car ls)) v))))))然后我们看到函数p同样是一个非尾部调用,那我们进一步将传递给p的隐式的 continuation显式的写出来:
(cons (p (car ls)) v) => (p (car ls) (lambda (n) (k (cons n v))))所以可以得到map1的CPS版本:
(define map1 (lambda (p ls k) (if (null? ls) (k '()) (map1 p (cdr ls) (lambda (v) (p (car ls) (lambda (n) (k (cons n v)))))))))在这里,函数p接受continuation,这个continuation是执行将p的结果cons到 map1的结果中,而在p函数中会根据是否产生异常来决定是使用这个 continuation还是退出的continuation。又因为map1在 reciprocals 中是一个 尾部调用,所以直接传递延续即可,所以reciprocals的任务就是向map1中 传递正确的p函数、延续以及退出的延续(就是map1开始前的延续):
(define reciprocals (lambda (ls k) (let ([break k]) (map1 (lambda (x k) (if (= x 0) (break "error") (k (/ 1 x)))) ls k))))测试结果:
(reciprocals '(2 1/3 5 1/4) (lambda (x) x)) => (1/2 3 1/5 4) (reciprocals '(2 1/3 0 5 1/4) (lambda (x) x)) => "error"最后简化一下,把map1写成内部定义的形式:
(define reciprocals (lambda (ls k) (let ([break k]) (let map1 ([p (lambda (x k) (if (= x 0) (break "error") (k (/ 1 x))))] [ls ls] [k k]) (if (null? ls) (k '()) (map1 p (cdr ls) (lambda (v) (p (car ls) (lambda (n) (k (cons n v)))))))))))