【问题标题】:call-with-current-continuation - state saving conceptcall-with-current-continuation - 状态保存概念
【发布时间】:2014-08-02 17:29:54
【问题描述】:

阅读经验丰富的计划者后,我觉得我理解了call/cc。但是,在看到call/cc 的一些 WOW 技巧后,我发现我错了。

(define cc 0)
(define (f)
  (call/cc (lambda (k)
             (set! cc k)
             3)))

(+ 1 2 4 (f)) ; eval's to 10
(cc 13) ; eval's to 20

这完全符合我的理解。我想当我接到call/cc 电话时,我只是在保存程序状态。并用函数调用它旁边的函数。如果该函数 (k) 是从某个地方调用的,那么我只需将整个 (call/cc ...) 替换为给它的参数即可。 上面的程序似乎也是这样工作的


但是,

(define (itr lst)
  (define (state k)
    (for-each (lambda (item)
                (call/cc (lambda (h)
                           (set! state h)
                           (k item))))
              lst)
    (k 'done))

  (define (generator)
    (call/cc (lambda (k) (state k))))
  generator)

(define (next)
  (itr (range 2)))

调用next 3 次产生0、1 和'done。这意味着当state 使用generator 给出的函数k 时,它并没有恢复程序的状态。 我只是向你展示了我试图理解它。


那么,call/cc 实际上是如何工作的?

【问题讨论】:

    标签: scheme continuations continuation-passing callcc


    【解决方案1】:

    带有延续传递样式(不带call/cc

    如果您实现一个使用显式延续传递样式而不是首先使用call/cc 的版本,则可能更容易理解此示例。在这种情况下,让我们从map 的延续传递版本开始:

    (define (kmap fn list k)
      (if (null? list)
          (k list)
          (fn (car list)
              (lambda (head)
                (kmap fn
                      (cdr list)
                      (lambda (tail)
                        (k (cons head tail))))))))
    
    (define (identity x) x)
    
    (kmap (lambda (x k) (k (+ 1 x))) '(1 2 3 4) identity)
    ;=> (2 3 4 5)
    

    如果您不熟悉连续传球风格,这可能会让您有点头晕,但也不会太难。请记住,kmapfn 在末尾都带有一个附加参数,应该使用“结果”调用。因此,当我们用(car list) 调用fn 时,我们还向它传递了一个过程(lambda (head) ...),它负责为我们处理其余的映射。映射的其余部分再次根据kmap 定义。对kmap 的每次调用都需要一个最终的延续,该延续期望接收将fn 映射到列表上的结果。

    现在,既然我们可以看到如何使用延续传递样式实现映射,我们可以使用它编写一个迭代器生成过程。过程iterator 接受一个列表并返回一个过程,我们可以调用该过程来获取list 的每个元素。

    (define (iterator list)
      (define (next k)
        (kmap (lambda (item more-next)
                (set! next more-next)
                (k item))
              list
              (lambda (results)
                (k 'done))))
      (lambda ()
        (next identity)))
    
    > (define get (iterator '(1 2)))
    > (get)
    1
    > (get)
    2
    > (get)
    done
    > (get)
    done
    
    > (define get (iterator '(a b c)))
    > (get)
    a
    > (get)
    b
    > (get)
    c
    > (get)
    done
    > (get)
    done
    

    这里的诀窍是我们定义了一个本地过程next。当list 的每个元素被处理为将处理list 剩余部分的过程时,它使用一个重新定义 next 的过程调用kmap。重新定义next 后,它使用元素调用k。传递给kmap 的最终延续实际上忽略了传递给它的结果,而只是使用符号done 调用k。我们从iterator返回的不是next 的值,而是一个调用next 并延续identity 的过程。这里的间接意味着我们总是用identity 调用nextlatest 值。传递identity 作为延续意味着我们只取回列表元素。

    call/cc

    现在我们知道如何在没有call/cc 的情况下做到这一点,我们可以更好地了解如何使用call/cc 来做到这一点。回忆一下问题中的定义:

    (define (itr lst)
      (define (state k)
        (for-each (lambda (item)
                    (call/cc (lambda (h)
                               (set! state h)
                               (k item))))
                  lst)
        (k 'done))
    
      (define (generator)                   
        (call/cc (lambda (k) (state k))))   
                                            
      generator)                            
    

    返回生成器

    首先要注意

      (define (generator)
        (call/cc (lambda (k) (state k))))
    
      generator
    

    可以简化为

    (lambda () (call/cc (lambda (k) (state k))))
    

    这就是我们在实施中所做的。当您从 REPL 调用它时,k 想要做的就是获取该值并返回它(并打印它)。在我们的版本中,我们通过简单地返回原样来近似它。也就是说,我们使用identity,我们使用名称next而不是state。所以

    (lambda () (call/cc (lambda (k) (state k))))
    

    就像

    (lambda () (next identity))
    

    state(或next)过程

    剩下的

      (define (state k)
        (for-each (lambda (item)
                    (call/cc (lambda (h)
                               (set! state h)
                               (k item))))
                  lst)
        (k 'done))
    

    也与我们所做的非常相似。我们不使用kmap 和带有两个参数(项目和延续)的fn,而是使用for-each,它接受单个参数(项目)的过程,在该过程中,我们使用call/cc抓住延续。所以

    (for-each
      (lambda (item)
        (call/cc (lambda (h)
                   ...
    

    就像

    (kmap (lambda (item h)
            ...
    

    for-each 不需要最终的延续参数,所以我们不能传递结果忽略(lambda () (k 'done))。相反,我们只是在for-each 调用之后调用(k 'done) 。也就是说,

    (for-each fn list)
    (k 'done)
    

    就像

    (kmap fn
          list
          (lambda (result)
            (k 'done)))
    

    保存程序状态

    在这两种实现中,您在某种意义上都在“保存程序状态”。您正在保存的重要状态是将继续迭代列表的状态。

    【讨论】:

    • 现在我知道什么是延续了。我之前曾与collector(小策划者使用这个术语)合作过,所以kmap 不是问题。唯一让我再次感到痛苦的是(set! next more-next)。现在这是一段历史。我明白了,你是那些可以轻松表达想法的人之一。非常感谢。
    【解决方案2】:

    您对某些问题的怀疑是正确的。代码完全被破坏了,这从生成器在调用该项目时无法捕获主线程序的新延续这一事实中显而易见。或者更确切地说,它摸索并扔掉了那个延续。结果是在尝试获取第二项时调用了错误的延续,导致无限循环。

    首先,让我们更正您问题的措辞。调用next 不会产生项目;调用 next 会产生生成器函数。 next应该使用的方式是这样的:

    (let ((g (next)))
      (list (g) (g) (g)))  ;; should return (0 1 done)
    

    但是,实际上它行不通。让我们检查一下:

    (define (itr lst)
      (define (state k)
        (for-each (lambda (item)
                    (call/cc (lambda (h)
                               (set! state h)
                               (k item))))
                  lst)
        (k 'done))
    
      (define (generator)
        (call/cc (lambda (k) (state k))))
      generator)
    
    (define (next)
      (itr (range 2)))
    

    让我们追踪正在发生的事情。

    设置: 当调用(next) 时,表达式(iter (range 2)) 返回generator,这是在itrlststate 变量所在的环境中捕获的闭包被绑定了。

    第一次迭代:第一次调用由next 返回的生成器因此调用generator。现在generator 捕获它自己的延续,它在lambda 中显示为k,并将其传递给state。然后state 运行,并且generator 的延续绑定到k。它进入第一次迭代,并通过将自己替换为新的延续来保存自己的状态:(set! state h)。此时,statedefine-d函数的先前绑定被覆盖; state 现在是恢复 for-each 的延续函数。下一步是将item 产生回k 延续,这使我们回到返回项目的generator。太好了,这就是第一次调用(next) 时第一个项目的显示方式。

    第二次迭代:从这里开始,事情就出错了。再次由next 返回的对生成器的第二次调用再次捕获一个延续并调用state,这现在是生成协程的延续。生成器将自己的延续传递给state。但是state 不再是define-d by itr 的函数! 因此,generator 中新捕获的延续不与 k 参数连接,该参数位于 for-each 的词法范围内。 当调用 (k item) 以产生第二项时,这个k 仍然引用原始的k 绑定,它在第一次调用generator 时保存最初捕获的延续。 这类似于向后跳转并导致非终止行为

    以下是我们可以解决的方法:

    (define (itr lst)
      (define yield '()) ;; forward definition (could use let for this).
    
      (define (state)    ;; k parameter is gone
        (for-each (lambda (item)
                    (call/cc (lambda (h)
                               (set! state h)
                               (yield item))))  ;; call yield, not k
                  lst)
        (yield 'done))  ;; yield, not k.
    
      (define (generator)
        (call/cc (lambda (self) 
                   (set! yield self) ;; save new escape on each call
                   (state))))
      generator)
    
    ;; test
    (let ((g (itr (range 2))) ;; let's eliminate the "next" wrapper
      (display (list (g) (g) (g))))
    

    输出是(0 1 done)

    【讨论】:

      猜你喜欢
      • 2019-03-19
      • 2011-08-03
      • 2011-04-18
      • 2011-07-03
      • 2013-05-15
      • 2012-10-21
      • 1970-01-01
      • 2020-05-23
      • 2017-11-28
      相关资源
      最近更新 更多