调用 cc 示例球拍

call cc example racket

我正在分析这段代码关于 call/cc 的使用。这个功能有点神秘,要完全理解还是比较复杂的。

我真的无法理解这段代码是如何工作的。以下是我的解读。

(define (print+sub x y)
  (display x)
  (display " ")
  (display y)
  (display " -> ")
  (- x y))

(define (puzzle)
  (call/cc (lambda (exit)
             (define (local e)
               (call/cc
                (lambda (local-exit)
                  (exit (print+sub e
                           (call/cc
                            (lambda (new-exit)
                              (set! exit new-exit)
                              (local-exit #f))))))))
             (local 6)
             (exit 2))))

(define x (puzzle))

call/cc通过

调用
    call/cc (lambda(exit))

然后再次通过

              (call/cc
                (lambda (local-exit)

函数 local 使用参数 6 调用,参数 6 作为 x 传递给 print+sub。但是值 2 如何作为 y 到达 print+sub

最重要的是,所有这些指令的执行顺序是什么?

调用 (puzzle) 建立一个延续 exit 这样调用 (exit val) 就好像 那个调用 (puzzle)刚刚 return 编辑了 val 值。

然后调用(local 6)。它设置了一个延续 local-exit,这样调用 (local-exit val2) 就好像 那个调用 (local 6) 刚刚 return 编辑了那个 val2值。当然 return 值会被忽略,下一次调用 (exit 2) 将被调用。

现在,设置local-exit后,调用(exit (print+sub e ...))。它需要先找出 (print+sub e ...) 的值 val3,然后才能将其传递给调用 (exit val3).

print+sub 需要两个参数。该调用有两个必须计算的表达式,因此找到的值(如果有)将作为 xy 传递给 print+sub.

评估e很简单。是6.

计算第二个表达式,(call/cc (lambda (new-exit) ...)),建立另一个延续,new-exit,这样调用 (new-exit y) 等同于 returning y 进入那个插槽 {y} 在呼叫中等待它 (print+sub 6 {y}).

然后是正文

      (lambda (new-exit)
          (set! exit new-exit)
          (local-exit #f))

已进入。 (set! exit new-exit) 从现在开始将任何调用 (exit val) 的含义更改为与调用 (new-exit val) 相同。

现在,终于调用了 (local-exit #f)。它跳出 (local 6) 调用, 立即 return 调用 #f,然后被忽略。调用 (exit 2)。这与调用 (new-exit 2) 相同。这意味着 returning 2 进入那个 {y} 插槽,所以调用 (print+sub e 2) inside (exit (print+sub e 2))现在执行。

print+sub 打印它打印的内容和 returns 4,因此现在调用 (exit 4)

现在关键的花絮是,这里使用的 exit 的值是多少?是原来的exit续作,还是改的new-exit?

假设 Scheme 标准说在任何函数应用程序中 (foo a1 a2 ... an) 首先评估 foo,然后 然后 ai 以未指定的顺序求值, 然后 将函数值应用于刚刚找到的 n 参数值。这意味着要调用的 exit 是原始的 exit 延续,因此值 4 被 return 编辑为原始调用的最终值 (puzzle)(这就是 DrRacket 中真正发生的事情)。

假设 Scheme 标准没有这样说。那么 exit 现在实际上可能是 new-exit。因此调用它会导致无限循环。这不是 DrRacket 中发生的事情。

的确,如果我们将 exit 替换为 (lambda (v) (exit v))

           ((lambda (v) (exit v))
                (print+sub e
                           (call/cc
                            (lambda (new-exit)
                              (set! exit new-exit)
                              (local-exit #f))))))))

代码确实进入了无限循环。


Continuations 就像带有值的跳转 (GOTO)。当我们有一些像 ...... (foo) ..... 和普通函数 foo 的代码时,当 foo 的计算结束时,returned 值将在该代码中进一步使用,根据什么写在那里。

puzzle用作foo,计算过程相同。 Scheme 试图找出 puzzle 的 return 值,以便在周围的代码中进一步使用它。

但是 puzzle 立即调用 call/cc,所以它创建了这个标记,一个要转到的 GOTO 标签,所以当 / if / 深入 puzzle 一个调用 (exit 42),控件跳转到 - 转到 - 那个标记,那个标签,42 用作 return 值.

因此,当在 (puzzle) 深处调用 (exit 42) 时,它的效果就好像对 (puzzle) 的调用刚刚 return 与 42 作为 return 值放入其周围的代码中,而无需遍历 puzzle.

中的所有剩余代码

这就是延续的工作原理。 continuation 是一个跳转到的标记,带有一个值,将在后续代码中使用,就好像 return 被前面的代码正常编辑一样。


使用 Racket 的 let/cc 或等效的宏可以使代码更容易阅读:

(define-syntax with-current-continuation    ; let/cc
  (syntax-rules ()
    ((_ c a b ...)
     (call/cc (lambda (c) a b ...)))))

(define (puzzle2)
  (let/cc exit  ; --->>--------------->>------------>>-------------.
    (define (local e)                                            ; |
      (let/cc local-exit  ; --->>----------------------------.     |
        (exit (print+sub e                                 ; |     |
                         (let/cc new-exit  ;  -->>----.      |     |
                           (set! exit new-exit)    ;  |      |     |
                           (local-exit #f))        ;  |      |     |
                                          ;; --<<-----*      |     |
                         )))                               ; |     |
                           ;; --<<-----------------<<--------*     |
      )                                                          ; |
    (local 6)                                                    ; |
    (exit 2))                                                    ; |
            ;; --<<---------------<<------------------<<-----------*
  )

假设您在调试器中,并且在每个 let/cc 表单的 右括号 上放置了一个断点。每个延续,如果被调用,直接跳转到其定义的 let/cc 的右括号,以便传递的值在后续计算中用作该表达式的 return 值。基本上就是这样。

令人费解的部分是,在 Scheme 中,您可以从 外部 跳转到右括号,从而重新进入旧的控制上下文。