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

 

posted @ 2018-12-21 22:00  lsgxeva  阅读(3367)  评论(0编辑  收藏  举报