2

我正在分析有关使用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)

使用传递给aslocal的参数调用该函数。但是值如何到达as ?6print+subx2print+suby

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

4

1 回答 1

2

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

于 2020-12-15T13:26:28.263 回答