调用 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
需要两个参数。该调用有两个必须计算的表达式,因此找到的值(如果有)将作为 x
和 y
传递给 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 中,您可以从 外部 跳转到右括号,从而重新进入旧的控制上下文。
我正在分析这段代码关于 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
需要两个参数。该调用有两个必须计算的表达式,因此找到的值(如果有)将作为 x
和 y
传递给 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 中,您可以从 外部 跳转到右括号,从而重新进入旧的控制上下文。