来源 https://www.sczyh30.com/posts/Functional-Programming/call-with-current-continuation/html
Continuation 也是一个老生常谈的东西了,咱们来回顾一下。首先咱们看一下 TSPL4 中定义的表达式求值须要作的事:git
During the evaluation of a Scheme expression, the implementation must keep track of two things: (1) what to evaluate and (2) what to do with the value.express
Continuation 即为其中的(2),即表达式被求值之后,接下来要对表达式作的计算。R5RS 中 continuation 的定义为:ide
The continuation represents an entire (default) future for the computation.函数
好比 (+ (* 2 3) (+ 1 7))
表达式中,(* 2 3)
的 continuation 为:保存 (* 2 3)
计算出的值 6
,而后计算 (+ 1 7)
的值,最后将两表达式的值相加,结束;(+ 1 7)
的 continuation 为:保存 (+ 1 7)
的值 8
,将其与前面计算出的 6
相加,结束。post
Scheme 中的 continuation 是 first-class 的,也就是说它能够被当作参数进行传递和返回;而且 Scheme 中能够将 continuation 视为一个 procedure,也就是说能够调用 continuation 执行后续的运算。测试
每一个表达式在求值的时候,都会有一个对应的 current continuation,它在等着当前表达式求值完毕而后把值传递给它。那么如何捕捉 current continuation 呢?这就要用到 Scheme 中强大的 call/cc
了。call/cc
的全名是 call-with-current-continuation
,它能够捕捉当前环境下的 current continuation 并利用它作各类各样的事情,如改变控制流,实现非本地退出(non-local exit)、协程(coroutine)、多任务(multi-tasking)等,很是方便。注意这里的 continuation 将当前 context 一并打包保存起来了,而不仅是保存程序运行的位置。下面咱们来举几个例子说明一下 call/cc
的用法。网站
咱们先来看个最简单的例子 —— 用它来捕捉 current continuation 并做为 procedure 调用。call/cc
接受一个函数,该函数接受一个参数,此参数即为 current continuation。以以前 (+ (* 2 3) (+ 1 7))
表达式中 (* 2 3)
的 continuation 为例:ui
1
2
3
4
5
|
(
define cc #f)
(
+ (call/cc (lambda (return)
(
set! cc return)
(
* 2 3)))
(
+ 1 7))
|
咱们将 (* 2 3)
的 current continuation (用(+ ? (+ 1 7))
表示) 绑定给 cc
变量。如今 cc
就对应了一个 continuation ,它至关于过程 (define (cc x) (+ (x) (+ 1 7)))
,等待一个值而后进行后续的运算:lua
1
2
3
4
5
6
|
> cc
#<continuation>
> (cc 10)
18
> (cc (* 2 3))
14
|
这个例子很好理解,咱们下面引入 call/cc
的本质 —— 控制流变换。在 Scheme 中,假设 call/cc
捕捉到的 current continuation 为 cc
(位于 lambda
中),若是 cc
做为过程 直接或间接地被调用(即给它传值),call/cc
会当即返回,返回值即为传入 cc
的值。即一旦 current continuation 被调用,控制流会跳到 call/cc
处。所以,利用 call/cc
,咱们能够摆脱顺序执行的限制,在程序中跳来跳去,很是灵活。下面咱们举几个 non-local exit 的例子来讲明。
Scheme 中没有 break
和 return
关键字,所以在循环中若是想 break
并提早返回的话就得借助 call/cc
。好比下面的例子寻找传入的 list
中是否包含 5
:
1
2
3
4
5
6
7
8
9
10
11
12
|
(
define (do-with element return)
(
if (= element 5)
(
return 'find-five)
(
void)))
(
define (check-lst lst)
(
call/cc (lambda (return)
(
for-each (lambda (element)
(
do-with element return)
(
printf "~a~%" element))
lst)
'not-found)))
|
测试:
1
2
3
4
5
6
7
8
9
|
> (check-lst '(0 2 4))
0
2
4
'not-found
> (check-lst '(0 3 5 1))
0
3
'find-five
|
check-lst
过程会遍历列表中的元素,每次都会将 current continuation 传给 do-with
过程并进行调用,一旦do-with
遇到 5
,咱们就将结果传给 current continuation (即 return
),此时控制流会立刻跳回 check-lst
过程当中的 call/cc
处,这时候就已经终止遍历了(跳出了循环)。call/cc
的返回值为 'find-five
,因此最后会在控制台上打印出 'find-five
。
咱们再来看一个经典的 generator 的例子,它很是像 Python 和 ES 6 中的 yield
,每次调用的时候都会返回 list 中的一个元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
(
define (generate-one-element-at-a-time lst)
;; Hand the next item from a-list to "return" or an end-of-list marker
(
define (control-state return)
(
for-each
(
lambda (element)
(
set! return (call/cc
(
lambda (resume-here)
;; Grab the current continuation
(
set! control-state resume-here)
(
return element)))))
lst)
(
return 'you-fell-off-the-end))
;; This is the actual generator, producing one item from a-list at a time
(
define (generator)
(
call/cc control-state))
;; Return the generator
generator)
(
define generate-digit
(
generate-one-element-at-a-time '(0 1 2)))
|
调用:
1
2
3
4
5
6
7
8
|
> (generate-digit)
0
> (generate-digit)
1
> (generate-digit)
2
> (generate-digit)
'you-fell-off-the-end
|
注意到这个例子里有两个 call/cc
,你们刚看到的时候可能会有点晕,其实这两个 call/cc
各司其职,互不干扰。第一个 call/cc
负责保存遍历的状态(今后处恢复),而 generator
中的 call/cc
才是真正生成值的地方(非本地退出)。其中一个须要注意的地方就是 control-state
,它在第一次调用的时候仍是个 procedure,在第一次调用的过程当中它就被从新绑定成一个 continuation
,以后再调用 generator
生成器的时候,控制流就能够跳到以前遍历的位置继续执行下面的过程,从而达到生成器的效果。
continuation 环境嵌套。后面有时间专开一篇分析~
1
2
3
4
5
|
(
let* ((yin
((
lambda (cc) (display #\@) cc) (call-with-current-continuation (lambda (c) c))))
(
yang
((
lambda (cc) (display #\*) cc) (call-with-current-continuation (lambda (c) c)))))
(
yin yang))
|
这里简单提一下 call/cc
与类型系统和数理逻辑的联系。call/cc
的类型是 ((P → Q) → P) → P
,经过 Curry-Howard 同构,它能够对应到经典逻辑中的 Peirce’s law:
Peirce’s law 表明排中律 P∧¬PP∧¬P,这条逻辑没法在 λ演算所对应的直觉逻辑中表示(直觉逻辑中双重否认不成立),所以 call/cc
没法用 λ表达式定义。一般咱们用扩展后的 λμ calculusλμ calculus 来定义 call/cc
,λμ calculusλμ calculus 经 Curry-Howard 同构能够获得经典逻辑。
原文:https://blog.csdn.net/nklofy/article/details/48999417
这篇来自我在某问答网站上的一个回答。但我以为这个问题颇有价值,我思考和写做的时间花费了好久。因此我以为应该保存起来。
问题是,call/cc的类型是什么。咱们知道,(call/cc (lambda (k) p))有两种用法,一种是(call/cc (lambda (k) (k a))),例如(+ 1(call/cc (lambda (k) (k 2))));一种是(call/cc (lambda (k) k)),例如(let a (call/cc (lambda (k) k))) 或 (define (get-cc) (call/cc (lambda (c) c))) 。第一种返回a,并送入continuation计算;第二种直接返回一个continuation。
因此很明显,call/cc不是只有一种返回类型。两种用法对应两种不一样类型。第一个类型是((T1 -> T2) -> T1) -> (T1 -> T2) -> T1,第二种是( (T1 -> T2) -> (T1 -> T2) ) -> (T1 -> T2) -> (T1 -> T2)。
一个简单的联想是,假设x类型是T1->T2, a类型是T1,那么 ((λ(k) (k a) ) x)和 (λ(k) (k) ) x)分别返回什么类型?前者是T2,后者是T1->T2。这就是所谓函数一等公民。
回到call/cc。首先要分析整个程序到call/cc以前的位置,按照TAPL的表达方式,此时的continuation类型是:
λ k: T1 . T1 -> T2
从这个位置开始,call/cc被调用,evaluation rules可表示为:
call/cc [k |-> continuation ] λ k. k a --> a。 E-APP1 (第一种状况)
call/cc [k |-> continuation ] λ k. k --> continuation。 E-APP2 (第二种状况)
第一种状况下,type rule为:
Γ├ continuation: T1->T2 Γ├ a: T1 Γ├ λ k: T1 -> T2 . k a
-------------------------------------------------------------------------------------- T-APP1
Γ├ (call/cc ( λ k . k a )) continuation : T1
call/cc过程返回类型T1。缘由是evaluation rule对应的是E-APP1。
第二种状况下type rule:
Γ├ continuation: T1->T2 Γ├ λ k: T1 -> T2 . k
-------------------------------------------------------------------------------------- T-APP2
Γ├ (call/cc ( λ k . k)) continuation : T1->T2
即该状况下应该返回T1->T2。
因此结论就是,两种状况下,返回的类型是不同的。call/cc有两种可能的返回类型,返回哪种根据不一样的(λ (k) process)匹配。一种匹配 k a,另外一种匹配 k。是的,这就相似于(λ x . x a ) 与(λ x . x ) 的区别,返回类型不同很正常。
在第二种状况下process 直接返回k,但其实程序中call/cc 先后一般会有个let、set!或者其余赋值函数,将这个continuation保存到某个变量x,程序后面会调用(x a)的形式。对于(x a)或者以前的(continuation a),都回到了(T1 -> T2) -> T1 -> T2的套路上,程序最终运行完时两种状况异曲同工。
PS,在我看来,call/cc更接近于宏而非函数,每每纯用于结构跳转而非求值,例如call/cc (lambda (k) (if(something) (k #t)))...)这种用法。它的精华放在(k #t)之外的地方,控制运行仍是跳转。还有,scheme原本就是动态类型系统,类型能够任意的变,分析起来很是痛苦。若看成宏来看就顺眼多了,(...call/cc ...)这个括号里的内容总体用k一个符号代替。而后不管哪一种用法,遇到k a或k b时,从整个程序中挖掉call/cc括号内容后,a或b代入k所在位置就能获得结果。
参考文献:<types and programming languages>
================= End