call/cc 总结 | Scheme
call/cc 总结 | Scheme
来源 https://www.sczyh30.com/posts/Functional-Programming/call-with-current-continuation/
Continuation
Continuation 也是一个老生常谈的东西了,我们来回顾一下。首先我们看一下 TSPL4 中定义的表达式求值需要做的事:
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.
Continuation 即为其中的(2),即表达式被求值以后,接下来要对表达式做的计算。R5RS 中 continuation 的定义为:
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
相加,结束。
Scheme 中的 continuation 是 first-class 的,也就是说它可以被当做参数进行传递和返回;并且 Scheme 中可以将 continuation 视为一个 procedure,也就是说可以调用 continuation 执行后续的运算。
call/cc
每个表达式在求值的时候,都会有一个对应的 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
我们先来看个最简单的例子 —— 用它来捕捉 current continuation 并作为 procedure 调用。call/cc
接受一个函数,该函数接受一个参数,此参数即为 current continuation。以之前 (+ (* 2 3) (+ 1 7))
表达式中 (* 2 3)
的 continuation 为例:
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)))
,等待一个值然后进行后续的运算:
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 的例子来说明。
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
生成器的时候,控制流就可以跳到之前遍历的位置继续执行下面的过程,从而达到生成器的效果。
call/cc的类型是什么
原文:https://blog.csdn.net/nklofy/article/details/48999417
这篇来自我在某问答网站上的一个回答。但我觉得这个问题很有价值,我思考和写作的时间花费了很久。所以我觉得应该保存起来。
(call/cc (lambda (k) p)) 表达式中 call/cc 的是什么? scheme 允许任意表达式的 continuation 通过 call/cc 过程来进行捕获。call/cc 接收一个单参数过程 p,并将 current continuation 的具体表示传递给过程 p,continuation 本身使用过程来表示,这里记作 k。每当 k 被应用到一个值时,它会将这个值返回到调用 call/cc 处的 continuation,这个值就成为了调用 call/cc 的结果值。如果 k 没有被调用,过程 p 的返回值将作为 call/cc 表达式的值。
(call/cc (lambda (k) p)) 表达式中 call/cc 的类型是什么? 我们知道 (call/cc (lambda (k) p)) 有两种用法。
一种是 (call/cc (lambda (k) (k a))) 返回 a,并送入 continuation 计算; 例如 (+ 1(call/cc (lambda (k) (k 2)))) ,返回 2 。
一种是 (call/cc (lambda (k) k)) 直接返回一个 continuation ; 例如 (let a (call/cc (lambda (k) k))) 或 (define (get-cc) (call/cc (lambda (c) c)))
所以很明显,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) 分别返回什么类型?前者是T1,后者是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 (第二种情况)
第一种情况下 ((λ(k) (k a) ) x) ,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。
第二种情况下(λ(k) (k) ) x) ,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>
来源 https://www.zhihu.com/question/21954238/answer/1829986581
大佬们的回答都比较理论,不精通lambda calculus很难看懂,我来写一个从实际运用方面的理解。
下面我将从可以干啥、用来干啥、如何理解、具体用法这四个方面对该问题进行回答,并给你带来一些精巧的例子。
1. call/cc
可以干啥呢?
它能够执行类似C语言的setjmp longjmp那样的非局部跳转,不过比setjmp longjmp强大。
2. 我们可以用call/cc来干啥?
有了call/cc
, 我们可以避免全局尾递归+CPS(Continuation Passing Style)的写法,来轻易实现用Scheme模拟C风格命令式的return
break
continue
try catch
编程范式。此外,call/cc还可以用来实现“轻量级进程”(参考 Chez Scheme 作者 Dybvig 大佬的The Scheme Programming Language 一书Section 3.3. Continuations一节),等等。
我在这里把代码贴出来,供平日学习参考
(define lwp-list '())
(define lwp
(lambda (thunk)
(set! lwp-list (append lwp-list (list thunk)))))
(define start
(lambda ()
(let ([p (car lwp-list)])
(set! lwp-list (cdr lwp-list))
(p))))
(define pause
(lambda ()
(call/cc (lambda (k) (lwp (lambda () (k #f))) (start)))))
下面是“轻量级进程”运行示例
(lwp (lambda () (let f () (pause) (display "h") (f))))
(lwp (lambda () (let f () (pause) (display "e") (f))))
(lwp (lambda () (let f () (pause) (display "y") (f))))
(lwp (lambda () (let f () (pause) (display "!") (f))))
(lwp (lambda () (let f () (pause) (newline) (f))))
(start) => hey!
hey!
hey!
hey!
...
3. 怎么理解call/cc?
call/cc 的全称是call-with-current-continuation,可以当做是理解它的关键吧。要理解什么是call-with-current-continuation,我们首先需要理解什么是continuation。continuation是什么呢?它就是当前任务(执行步骤)的后续(或者称延续,不过我觉得后续一词在字面意思上更为恰当。当然,后续看起来不像一个名词,所以我一般称作后续任务),也就是“完成当前任务之后,接着要做的事”,这就是continuation,其实你也可以把它看作做某段程序的入口。
那么call/cc干什么呢?它把当前的continuation以某种方式打包(我们暂且把这个continuation命名为 )传入一个接受
个参数的lambda,在这个
lambda
里面你可以把 当做一个转移来呼叫。当你像调用函数一样调用
的时候,就成功执行了呼叫转移,当前任务终止,
接着执行(别忘了
是你传入的某个continuation),不再回来。它不像函数执行那样,压栈,返回,它不压栈,更不返回。这其实是更换了执行流,当前平行世界突然就消失了,被另一个世界线代替。在Scheme里面,
可以接受任意个值(参数),进行执行流转移。当值的个数超过
时,使用
let-values
对传入continuation的值进行绑定
> (let-values
([(a b c d e)
(call/cc
(lambda (k)
(k 1 2 3 4 5)))])
(+ a b c d e))
15
> (let ([x (call/cc
(lambda (k)
(k 1)))])
x)
1
> (call/cc
(lambda (k)
(k)))
>
4. 下面我们来看看call/cc的一些高级用法
1. 模拟C语言的while循环,当然,写个for循环也是很容易的,只是while看起来比较短小,更适合用来理解call/cc
(define-syntax while
(lambda (x)
(syntax-case x ()
[(while condition body ...)
(with-syntax
([break (datum->syntax (syntax while) 'break)]
[continue (datum->syntax (syntax while) 'continue)])
(syntax
(call/cc
(lambda (break)
(let continue ()
(when condition body ... (continue)))))))])))
试用一下
(let ([i 0] [m 0])
(while (< i 100)
(when (< i m)
(set! i (+ i 1))
(continue))
(when (> i 50)
(printf "terminal at ~a\n" i)
(break))
(let ([j 1])
(while (<= j i)
(printf "~a" j)
(set! j (* j 2))
(if (<= j i)
(printf " ")
(printf ", "))))
(printf "~a\n" i)
(set! m (* 2 i))
(set! i (+ i 1))))
打印出
0
1, 1
1 2, 2
1 2 4, 4
1 2 4 8, 8
1 2 4 8 16, 16
1 2 4 8 16 32, 32
terminal at 64
2. 模拟命令式编程语言中的return
(define-syntax function
(lambda (x)
(syntax-case x ()
[(function (name . args) body ...)
(identifier? #'name)
(with-syntax
([return (datum->syntax #'function 'return)])
(syntax
(define name
(lambda args
(call/cc (lambda (return) body ...))))))])))
(function (fib n)
(if (< n 2) (return n))
(+ (fib (- n 1))
(fib (- n 2))))
这个写法其实意义不大,能减少一些嵌套括号,但会影响性能,因为每次函数调用都会调用一次call/cc。不过在循环(尾递归)里面return还是挺方便的。
3. 跳过函数调用栈直接break返回
我们定义一个名为with-break
的宏,通过该宏定义的函数f
,可以使用宏显式提供的名为break
的 continuation。break
是在f
被调用时隐式传入的f
调用处的continuation。当break
被呼叫时,执行流将立即转移到break
,所有递归调用将被舍弃,如下(暂未找到方法来消除函数体的复制)
(define-syntax with-break
(lambda (x)
(syntax-case x ()
[(with-break (name . args) body ...)
(identifier? #'name)
(with-syntax
([break (datum->syntax #'with-break 'break)])
(syntax
(define name
(lambda args
(call/cc
(lambda (break)
(set! name (lambda args body ...))
body ...))))))])))
先包装一下乘法运算函数*
,以显示一些运行时信息
(define (* x y)
(let ([r (#%* x y)])
(printf "~a * ~a = ~a\n" x y r)
r))
如果我们不使用break
(with-break (product xs)
(cond
[(null? xs) 1]
[(= (car xs) 0) 0]
[else (* (car xs) (product (cdr xs)))]))
(printf "~a\n" (product '(1 2 3 4 5 0 5 6 7 8 9)))
那么函数运行时,因为要逐层弾栈返回,所以上述代码将打印出
5 * 0 = 0
4 * 0 = 0
3 * 0 = 0
2 * 0 = 0
1 * 0 = 0
0
如果我们使用break
(with-break (product xs)
(cond
[(null? xs) 1]
[(= (car xs) 0) (break 0)]
[else (* (car xs) (product (cdr xs)))]))
(printf "~a\n" (product '(1 2 3 4 5 0 5 6 7 8 9)))
那么上述代码将只打印出结果
0
因为这次,break
被呼叫了,执行流立即转移到product
被调用处的continuation,也即是printf
语句,product
的递归调用不再逐层弾栈返回。
4. 模拟 try catch 进行异常处理
(define-syntax try
(lambda (x)
(syntax-case x (catch)
[(try body (catch (e) handle-error ...))
(with-syntax
([print (datum->syntax (syntax try) 'print)]
[tostr (datum->syntax (syntax try) 'tostr)])
(syntax (call/cc
(lambda (k)
(with-exception-handler
(lambda (e)
(define (tostr e)
(apply format
(cons (condition-message e)
(condition-irritants e))))
(define (print e)
(display (tostr e))
(newline))
handle-error
...
(k e))
(lambda () body))))))])))
该try catch宏的使用示例,测试运行结果是否符合期望
(define (test-file-name i)
(string-append "tests/test" (number->string i) ".js"))
(define (check-expect i expected)
(printf "Test~2s: " i)
(try (let ([filename (test-file-name i)])
(let ([observed (js-run-file filename)])
(if (eq? observed expected)
(printf "pass. ~a\n" observed)
(printf "fail! ~a\n" observed))))
(catch (e)
(if (eq? expected 'error)
(printf "pass. ")
(printf "fail! "))
(print e))))
5. 写解释器时,方便对throw进行catch,不然会很麻烦
(define js-exec-throw
(lambda (ast env catch)
(let ([value (js-eval (throw-value ast) env catch)])
(cond
[catch (catch value)]
[else (error 'js-exec-throw "throw: ~a" value)]))))
(define js-exec-try
(lambda (ast env k return break continue catch)
(let ([body (try-body ast)]
[catch-entry (try-entry ast 'catch)]
[finally-entry (try-entry ast 'finally)])
(define (finally)
(if finally-entry
(let ([body (finally-body finally-entry)])
(js-exec-block body env k return break continue catch))
(k env)))
(call/cc
(lambda (k)
(js-exec-block body env (lambda (_) (k (finally))) return break continue
(lambda (value)
(if catch-entry
(let ([ident (catch-ident catch-entry)]
[body (catch-body catch-entry)])
(js-exec-sequence body
(add-bind-to-environment ident value (push-new-frame env))
(lambda (x) x) return break continue catch)))
(k (finally)))))))))
前面定义的while循环有些问题,下面是改正之后的版本。在前面的while循环中,当我们在循环内部调用continue来跳过一段代码时,这种调用是非尾递归调用,待到循环条件为假,不再继续递归时,程序会返回到continue调用之处,继续执行其后面的代码。因此,我们想通过continue来跳过部分代码的预期行为将不能被正确地实现。
我们可以在每次循环时调用一次call/cc,来达到正确的continue行为,但是这种实现方式对性能的损耗太大。所以我选择了下面这种多分支执行,但是当某一分支执行到循环条件为假时就break的做法。
(define-syntax while
(lambda (x)
(syntax-case x ()
[(while condition body ...)
(with-syntax
([break (datum->syntax (syntax while) 'break)]
[continue (datum->syntax (syntax while) 'continue)])
(syntax
(call/cc
(lambda (break)
(let continue ()
(cond
[condition body ... (continue)]
[else (break)]))))))])))
上面这种修正方案虽然可行,但是有一个缺点:对continue的非尾递归调用(隐藏在宏内部while循环体最后对continue的尾递归调用之外,对continue的显式调用)会压栈,保存函数调用上下文。如果continue被大量调用,那么递归深度就会线性增加,GC时间将成为整个循环执行所耗时间的主要部分,严重影响性能。
另一种解决方案是:在循环执行循环体之前,调用一次call/cc 将continue保存为“循环执行循环体代码”这一continuation。这样,每次调用continue就会不保存上下文地跳转到while循环的条件判断处,进行新一轮循环
(define-syntax while
(lambda (x)
(syntax-case x ()
[(while condition body ...)
(with-syntax
([break (datum->syntax (syntax while) 'break)]
[continue (datum->syntax (syntax while) 'continue)])
(syntax
(call/cc
(lambda (break)
(define continue #f)
(call/cc (lambda (k) (set! continue k)))
(let loop ()
(when condition body ... (loop)))))))])))
这一方案依然并不完美,因为continuation的调用虽然比函数调用省时(因为不保存上下文),但依然没有尾递归调用高效。 完美方案大概只有把while宏写得具有自动CPS的功能。
================= End
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
2017-12-21 使用 GDB 调试多进程程序
2017-12-21 操作系统标识宏
2017-12-21 gdb调试多线程程序总结
2017-12-21 boost 1.56.0 编译及使用
2017-12-21 c++ bind1st 和 bind2nd的用法