call-with-current-continuation - 状态保存概念

时间:2014-06-12 11:46:52

标签: scheme continuations continuation-passing callcc

阅读 The Seasoned Schemer 后,我觉得我理解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使用k给出的函数generator时,它不会恢复程序的状态。 我刚刚向您展示了我试图了解它。


那么,call/cc实际上如何运作?

2 个答案:

答案 0 :(得分:7)

持续传递风格(不含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每个都会在结尾处使用"结果"来调用另一个参数。因此,当我们使用fn致电(car list)时,我们还会向其传递一个程序(lambda (head) ...),该程序负责为我们处理其余的映射。映射的其余部分再次以kmap定义。对kmap的每次调用都会进行最后一次延续,期望接收列表fn上的列表。

现在,由于我们可以看到如何使用continuation传递样式实现映射,我们可以使用它编写迭代器生成过程。过程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。当kmap的每个元素被处理为将处理{{1}的剩余部分的过程时,它会使用 redfines next的过程调用list }。重新定义list后,它会使用该元素调用next。传递给k的最后一个延续实际上忽略了传递给它的结果,只需使用符号kmap调用k。我们从done 返回的内容不是iterator的值,而是使用延续next调用next的过程。这里的间接意味着我们始终使用identity调用next最新值。传递identity作为延续意味着我们只需返回列表元素。

使用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

那就是我们在实施中所做的事情。当你从REPL调用它时,(lambda () (call/cc (lambda (k) (state k)))) 想要做的就是获取值并将其返回(并打印出来)。在我们的版本中,我们通过简单地返回它来改变它。也就是说,我们使用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)) 而不是使用kmapfn来获取两个参数(项目和延续),而是使用for-each来获取单个参数(项目)的过程并在该过程中,我们使用call/cc来获取延续。所以

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

就像

(kmap (lambda (item h)
        ...

for-each不需要最后的延续参数,因此我们无法通过结果忽略(lambda () (k 'done))。相反,我们只需在{/ 1}} (k 'done)电话后调用for-each 即可。也就是说,

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

就像

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

保存程序状态

在这两种实施方式中,您都可以保存程序状态"在某种意义上。您正在保存的重要状态是将继续迭代列表的状态。

答案 1 :(得分:1)

你怀疑某事是错的是正确的。代码完全被破坏,这是显而易见的,因为无论何时调用该项目,生成器都无法捕获主线程序的新延续。或者更确切地说,它会晃悠并抛出那种延续。结果是在尝试获取第二个项目时调用了错误的延续,导致无限循环。

首先,让我们从问题的措辞中纠正一些问题。调用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,这是在itr的环境中捕获的闭包, <{1}}和lst变量绑定。

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

第二次迭代:从现在开始,出现问题。第二次调用由(next)再次返回的生成器,再次捕获一个延续并调用next,这现在是生成协同例程的延续。生成器将自己的延续传递给state。但是state不再是state - define的功能! 因此,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)