【问题标题】:call cc example racket呼叫 cc 示例球拍
【发布时间】:2021-03-26 00:54:51
【问题描述】:

我正在分析这段代码关于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 调用,该参数作为x 传递给print+sub。但是2 的值如何以y 的形式到达print+sub

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

【问题讨论】:

标签: scheme racket continuations callcc


【解决方案1】:

调用(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 需要两个参数。该调用有两个必须计算的表达式,因此找到的值(如果有)将作为xy 传递给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 中,您可以从该表单外部跳转到结束括号,从而重新进入旧的控制上下文。

【讨论】:

  • 感谢您的详细解答!请问您是否可以进一步解释“调用(拼图)设置一个连续退出,使得调用(退出 val)与该调用(拼图)刚刚返回该 val 值相同。”是什么意思?
猜你喜欢
  • 2012-01-07
  • 1970-01-01
  • 2017-03-01
  • 1970-01-01
  • 2019-08-02
  • 1970-01-01
  • 2015-05-11
  • 2012-02-16
  • 2012-06-30
相关资源
最近更新 更多