调用(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
和y
to传递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
ai
n
exit
exit
4
(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
结束时,返回的值会根据那里写的内容在该代码中进一步使用。
使用puzzle
asfoo
时,评估进行相同。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 中,您可以从该表单之外跳转到结束括号,从而重新进入旧的控制上下文。