Continuation
continuation
这是一份关于continuation的文档,
将continuation讲的非常清楚,我简要的总结下我的理解: 先说几个概念与符号:
escape procedure: 它和普通的procedure是一样的,而且也有相同的行为,会返回相
同的值,唯一的不同的是这个函数它返回后就会替换调用栈,也就是说它会返回解释器
的最顶层或者解释器的REPL循环, 一个普通的procedure对应的escape procedure通 过在符号后添加^ 来表示,比如k
是一个普通的procedure, 那么它对应的escape procedure就是k^
, 还有lambda
是用来构建普通的procedure, 那么lambda^
就是用来构建escape procedure
.(注意lambda^
不是scheme的一 部分)下面举个例子:1
2
3(define k
(lambda (x y)
(+ x y)))现在假设你使用
(+ 3 (k 1 2))
那么结果是6, 但是如果你使用(+ 3 (k^ 1 2))
它就会返回3
, 原因是k^
与k
虽然有相同的返回值,但是它会替换 调用栈,所以也就不会执行后面的(+ 3)
,而是直接返回k^
的值continuation: 通俗点说
continuation
实际就是代表接下来要做的事或要进行的 操作, 也就是所谓的the rest of computation
, 所以当你要找一个函数来代表
某一点的continuation时,你只要弄清楚该点接下来要进行的操作,把这些操作封装
进一个函数就好 有了escape procedure
的概念后,那么continuation实际就是 一个escape
procedure.scheme的 call/cc
1
2
3(+ 2 (call/cc
(lambda (k^)
(* 5 (k^ 4)))))从上例可以看出
call/cc
的参数是一个lambda函数, 该函数也有一个参数(k^
), 很显然k^
是一个escape procedure, 当然k^
也代表当前的continuation
,在上例中,它的定义可以大致认为是这样的:1
2(lambda^ (v)
(+ 2 v))因为上例直接返回最顶层,所以可以直接这样
1
2(lambda (v)
(+ 2 v))所以当你使用
(k^ 4)
时, 它就返回6
, 同时替换调用栈,返回解释器的最上层,同
时从这个例子你也可以体会continuation的含义,
continuation就是接下来要做的 事或者操作, 那么上例中call/cc
之后接下来要做的事显然就是 (+
2 ret-of-call/cc),也就是加2continuation 本质上对应于栈, 是一种control context, 而environment是一种 data
context.continuation 内部是静态作用域的, 这个性质有时会产生一些很难捕捉的bug.
tail call(尾调用): 如果在函数p内调用了q, 而且q的返回值也是p的返回值, 那么 我们就说q是一个尾调用,
尾调用是不会增加栈的, 因为它本质上就是一个goto语句.
call/cc, let/cc
call/cc
[1]是 call-with-current-continuation
的缩写. 它的基本形式是这样的:
1 |
|
let/cc
可以看做是 call/cc
的一种简写:
1 |
|
call/cc
的参数是一个函数, 这个函数有一个参数, 这个参数会绑定到当前的 continuation
, 具体到这个例子就是:k
代表当前的continuation(也就是 call/cc
调用时的 continuation
), 现在当你应用该continuation
也就是使 用 (k 4)
时代码会立即从 call/cc
中返回, 并且返回值是 4
, 注意它不会执行
前面的 (* 5), 所以不要以为是返回 20. 下面在举几个例子:
1 |
|
再来看一个比较不好懂的例子
1 |
|
(call/cc (lambda(k) k))
返回当前的 continuation
, 我假设该continuation为cont
, 那么 cont
必然是一个escape procedure,而且可以看做是如下定义的:
1 |
|
所以原来的表达式也就等价于 ((cont (lambda(x) x)) "HEY!")
,又因为cont是一个 escape
procedure,所以上面的表达式又等价于 (cont (lambda(x) x))
,而该式很显 然返回 “HEY!“.
应用
BREAK与RESUME, 通过BREAK来暂停, 通过RESUME来从暂停的位置启动.
1
2
3
4
5(define BREAK
(lambda (message)
(call/cc (lambda (k^)
(set! RESUME K^)
((lambda^ (x) x) message)))))exceptions 比如racket的异常处理设施, with-handlers以及raise
1
2(with-handlers ([predicate-expr handler-expr] ...)
body ...+)下面是一个示例代码:
1
2
3
4(define (always-fail n)
(with-handlers ([even? (lambda (v) 'even)]
[positive? (lambda (v) 'positive)])
(raise n)))这种异常处理设施可以使用continuation来创建, 实际上就是将 body 部分放入 let/cc中,
调用raise实际就是调用continuation1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20(define convert
(lambda (exp)
(match exp
[`(with-handlers ([,preds ,handlers] ...)
,body ...)
`(let ([ret (let/cc k^
,@(map convert body))])
(cond
,@(map (lambda (p h) `[(,p ret) (,h ret)]) preds handlers)
))]
[`(raise ,val) `(k^ ,val)]
[`(lambda (,uvar ...) ,body ...)
`(lambda (,@uvar)
,@(map convert body))]
[`(,f ,v ...)
`(,(convert f)
,@(map convert v))]
[`(if ,test ,conseq ,alt)
`(if ,(convert test) ,(convert conseq) ,(convert alt))]
[x x])))上面这个函数就是将表达式中的with-handlers以及raise转换为continuation, 比 如下面的代码:
1
2
3
4
5
6
7
8
9
10
11(convert '(with-handlers ([even? (lambda (v) 'even)]
[positive? (lambda (v) 'positive)])
1
(raise 11)
2))
;; =>
;; '(let ((ret (let/cc k^ 1 (k^ 11) 2)))
;; (cond
;; ((even? ret) ((lambda (v) 'even) ret))
;; ((positive? ret) ((lambda (v) 'positive) ret))))当然这个函数还有一些问题,但是我只是想演示一下异常处理设施的实现原理.
generators
coroutine
CPS
CPS(continuation
passing style). 核心就是每一个函数都会带一个额外的参数
(continuation),前面说了continuation代表的是the rest of
computation, 因此这 个参数(continuation)代表了调用者需要对该函数的返回值进行的处理, 因此一个CPS
方式编写的函数最后都会使用函数的计算结果来调用你传递的那个continuation.
自动CPS转换
一个自动进行CPS转换的宏
1 |
|
自动的CPS转换的要点
首先任何表达式都要转换为 (lambda(k^) …) 的形式, 比如对一个常数表达式2, 你应该转换为 (lambda(k^) (k^
2))的形式, 这样做这是因为正在转换的表达式可 能是一个大的表达式中的子表达式, 所以应该把转换后的表达式传递给一个
continuation对于lambda表达式, 以(lambda(x) x)为例, 因为它实际和常量一样, 所以应该这样 转换:
1
2(lambda (k^)
(k^ (lambda (x) x)))但是cps转换后lambda表达式是要接受两个参数, 也就是x与一个continuation, 所 以:
1
2
3
4
5
6
7
8(lambda (k^)
(k^ (lambda (x dyn-k)
((lambda (k) (k x))
dyn-k))))
;;; a simple veraion
(lambda (k^)
(k^ (lambda (x dyn-k)
(dyn-k x))))注意上面的k^是lambda表达式定义时的continuation, 而dyn-k是lambda表达式应用
时的continuation,因为应用时continuation可以不同所以它也就是动态的.
CPS的应用
如果将一个解释器转换为CPS形式, 那么就可以很容易的实现像 scheme中call/cc,
let/cc这样可以获得当前continuation的结构, 因为解释器的continuation代表的解
释器接下来要进行的计算, 而解释器是用来模拟用户程序的, 所以实际上这个
continuation也可以看作是用户程序接下来要完成的计算,也就是用户程序当前的
continuation.
Footnotes
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!