调用(puzzle)设置了一个延续exit,使得调用(exit val)与该调用 (puzzle)刚刚返回该val值相同。
然后进行呼叫(local 6)。它设置了一个延续local-exit,使得调用(local-exit val2)与该调用 (local 6)刚刚返回该val2值相同。当然,该返回值被忽略,下一次调用(exit 2)将在接下来进行。
现在,设置完成后local-exit,就可以拨打电话(exit (print+sub e ...))了。它需要找出 first 的值val3,(print+sub e ...)以便将其传递给 call (exit val3)。
print+sub需要两个参数。该调用有两个必须计算的表达式,因此找到的值(如果有)将作为x和yto传递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)调用, 立即返回 that #f,然后被忽略。呼叫(exit 2)完成。就像拨打电话一样(new-exit 2)。这意味着返回2到那个{y}槽,所以现在执行(print+sub e 2) 里面的调用。(exit (print+sub e 2))
print+sub打印它打印的内容并返回4,所以(exit 4)现在调用它。
现在关键的花絮是,exit这里 used 的价值是什么?是原来的exit延续,还是改动后的new-exit?
假设 Scheme 标准规定,在任何函数中,首先计算 application,然后以未指定的顺序计算 s,然后将函数值应用于刚刚找到的参数值。这意味着要调用的 this 是原始延续,因此该值作为原始调用的最终值返回(这就是 DrRacket 中真正发生的情况)。(foo a1 a2 ... an) foo ainexitexit4(puzzle)
假设 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有值的跳转(a)。当我们有一些像...... (foo) .....普通函数一样的代码foo时,当评估foo结束时,返回的值会根据那里写的内容在该代码中进一步使用。
使用puzzleasfoo时,评估进行相同。Scheme 试图找出 的返回值,puzzle以便在周围的代码中进一步使用它。
但是立即puzzle调用call/cc,所以它创建了这个标记,一个GOTO要转到的标签,所以当 / if / 深入puzzle调用时(exit 42),控件跳转到 -转到- 那个标记,那个标签,并 42用作返回价值。
因此,当在内部(puzzle)进行调用时(exit 42),它的效果与该调用(puzzle)刚刚返回42其周围代码中的返回值相同,而无需遍历内部的所有剩余代码puzzle。
这就是延续的工作方式。延续是一个要跳转到的标记,带有一个值,将在后续代码中使用,就像前面的代码正常返回一样。
使用 Racketlet/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 中,您可以从该表单之外跳转到结束括号,从而重新进入旧的控制上下文。