调用(puzzle) 设置了一个延续exit,这样调用(exit val) 就如同那个调用 (puzzle) 刚刚返回了那个val 值。
然后调用(local 6)。它设置了一个延续 local-exit 以便调用 (local-exit val2) 与 该调用 (local 6) 刚刚返回该 val2 值相同。当然返回值会被忽略,接下来会调用(exit 2)。
现在,在设置local-exit 之后,调用(exit (print+sub e ...))。它需要先找出(print+sub e ...) 的值val3,以便将其传递给调用(exit val3)。
print+sub 需要两个参数。该调用有两个必须计算的表达式,因此找到的值(如果有)将作为x 和y 传递给print+sub。
评估e 很简单。是6。
评估第二个表达式 (call/cc (lambda (new-exit) ...)) 会设置另一个延续 new-exit,因此调用 (new-exit y) 等同于将 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) 调用,立即返回#f,然后将其忽略。拨打电话(exit 2)。就像拨打了(new-exit 2) 一样。这意味着将2 返回到该{y} 槽中,因此现在执行(print+sub e 2) inside (exit (print+sub e 2)) 的调用。
print+sub 打印它打印的内容并返回4,因此现在调用(exit 4)。
现在关键的花絮是,这里使用的exit 的值是多少?是原来的exit继续,还是修改后的new-exit?
假设Scheme标准规定在任何函数应用程序中(foo a1 a2 ... an)首先评估foo,然后以未指定的顺序评估ais,然后函数值应用于刚刚找到的n 参数值。这意味着要调用的 exit 是原始的 exit 延续,因此值 4 作为原始调用 (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))))))))
代码确实进入了无限循环。
延续就像带有值的跳转(GOTO)。当我们有一些像...... (foo) ..... 这样的代码和普通函数foo 时,当foo 的评估结束时,返回的值会根据那里写的内容在该代码中进一步使用。
将puzzle 用作foo,评估过程相同。 Scheme 尝试找出 puzzle 的返回值,以便在周围的代码中进一步使用它。
但是puzzle 立即调用call/cc,所以它创建了这个标记,一个GOTO 标签去,所以当/ if / 在puzzle 深处调用(exit 42) 时,控件跳转到 - 转到 - 那个标记,那个标签,42 被用作返回值。
所以当在(puzzle) 的深处调用(exit 42) 时,它的效果就像对(puzzle) 的调用刚刚以42 作为返回值返回到它的周围代码中一样,没有去通过puzzle中的所有剩余代码。
这就是延续的工作方式。延续是一个要跳转到的标记,带有一个值,将在后续代码中使用,就像前面的代码正常返回一样。
使用 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 的结束括号,以便在后续计算中将传递的值用作该表达式的返回值。基本上就是这样。
但令人费解的是,在 Scheme 中,您可以从该表单外部跳转到结束括号,从而重新进入旧的控制上下文。