scheme continuations for dummies

34
我真的无法理解continuations。我认为问题源于我不明白它们的作用是什么。我在书籍或网上找到的所有例子都非常琐碎。这让我想知道,为什么有人会需要continuations呢?
下面是一种典型的不切实际的例子,来自TSPL,我相信这是关于这个主题的一本相当受认可的书籍。在英语中,他们将continuation描述为对计算结果的“下一步操作”。好吧,这有点可以理解。
然后,给出了第二个例子:
(call/cc
  (lambda (k)
    (* 5 (k 4)))) => 4 

这有什么道理?k甚至没有定义!当无法计算(k 4)时,如何评估此代码?更不用说,call/cc如何知道撕下参数4到最内层表达式并返回它?(* 5 ..会发生什么?如果丢弃了最外层表达式,为什么还要编写它?
接着,一个“不那么”琐碎的例子是如何使用call/cc提供递归的非本地退出。这听起来像流程控制指令,即像命令式语言中的break/return,而不是计算。
这样做的目的是什么?如果有人需要计算结果,为什么不只是存储它,并在需要时调用它呢?

8
k isn't even defined! of course it is (lambda (k) ... ) - leppie
这是一个非本地退出的示例(意味着计算在完成之前被“中止”)http://eval.ironscheme.net/?id=99 - leppie
1
call/cc调用函数并将续延作为参数传递,因此该续延绑定到形式参数k,因此k 续延。 - uselpa
http://3e8.org/pub/scheme/doc/Call%20with%20Current%20Continuation%20patterns.pdf - David Brabant
2
“那听起来像是流程控制指令,就像命令式语言中的break/return。” “确切地说!” - Will Ness
显示剩余2条评论
4个回答

35

暂时不要考虑call/cc。每个编程语言中的表达式/语句都有一个延续——也就是你对结果所做的处理。例如,在C语言中,

x = (1 + (2 * 3)); 
printf ("Done");

数学作业的延续是 printf(...); (2 * 3) 的延续是 '加1; 赋值x; printf(...)'. 概念上,无论你是否能访问它,延续都存在。想一想你需要哪些信息来确定延续 - 这些信息包括1)堆内存状态(通常情况下),2)栈,3)任何寄存器和4)程序计数器。

因此,延续存在,但通常只是隐含的,并且无法访问。

在 Scheme 和其他一些语言中,你可以访问延续。本质上,在你不知情的情况下,编译器+运行时会捆绑所有需要的延续信息,将其存储(通常在堆中)并给你一个句柄。 你获得的句柄是函数 'k' - 如果你调用该函数,你将准确地在 call/cc 点后继续执行。重要的是,你可以多次调用该函数,并且每次都会在 call/cc 点之后继续执行。

让我们看一些例子:

> (+ 2 (call/cc (lambda (cont) 3)))
5

以上代码中,call/cc 的结果是 lambda 的结果,即3。但是续延并没有被调用。

现在让我们调用续延:

> (+ 2 (call/cc (lambda (cont) (cont 10) 3)))
12

通过调用continuation,我们跳过调用后面的任何内容,并从call/cc点继续执行。使用(cont 10),continuation返回10,它与2相加得到12。

现在让我们保存这个continuation。

> (define add-2 #f)
> (+ 2 (call/cc (lambda (cont) (set! add-2 cont) 3)))
5
> (add-2 10)
12
> (add-2 100)
102

保存 continuation 后,我们可以随意使用它来“跳回”到 call/cc 点之后的任何计算。

通常,continuation 用于非局部退出。想象一个函数将返回一个列表,除非出现某些问题,此时将返回 '()

(define (hairy-list-function list)
  (call/cc
    (lambda (cont)
       ;; process the list ...

       (when (a-problem-arises? ...)
         (cont '()))

       ;; continue processing the list ...

       value-to-return)))

2
(+ 1 (call/cc (lambda (cont) 3 )))(+ 1 (call/cc (lambda (cont) (cont 3) ))) 之间有什么微妙的区别?它们都返回相同的结果吗? - user2379016
3
call/cc的返回值要么是1)由lambda返回的值,要么是2)在继续被调用时提供的参数。在第一个例子中,lambda返回3,call/cc返回3,总结果为4。在第二个例子中,继续被调用时使用参数3,call/cc返回3,结果为4。 - GoZoner

9
这是我的课堂笔记中的文本:http://tmp.barzilay.org/cont.txt。它基于多个来源,并进行了大量扩展。它包含动机、基本解释、更高级的操作方法解释,以及从简单到高级的许多示例,甚至还有有关分界续延的快速讨论。

5

简而言之: Continuations(续体)基本上是带有值的捕获GOTO,就是这样。

你所询问的例子,

(call/cc
  (lambda (k)
    ;;;;;;;;;;;;;;;;
    (* 5 (k 4))                     ;; body of code
    ;;;;;;;;;;;;;;;;
    )) => 4 

可以大致翻译成例如Common Lisp的编程语言。
(prog (k retval)
    (setq k (lambda (x)             ;; capture the current continuation:
                    (setq retval x) ;;   set! the return value
                    (go EXIT)))     ;;   and jump to exit point

    (setq retval                    ;; get the value of the last expression,
      (progn                        ;;   as usual, in the
         ;;;;;;;;;;;;;;;;
         (* 5 (funcall k 4))        ;; body of code
         ;;;;;;;;;;;;;;;;
         ))
  EXIT                              ;; the goto label
    (return retval))

这只是一个例子;在Common Lisp中,我们无法在第一次退出PROG tagbody后再次跳回它。但在Scheme中,借助真正的continuations,我们可以做到。如果我们在被call/cc调用的函数体内设置了一些全局变量,比如(setq qq k),在Scheme中我们可以随时从任何地方调用它,重新进入相同的上下文(例如(qq 42))。
重点是,call/cc表达式的主体可能包含ifcond表达式。它只能在某些情况下调用continuation,在其他情况下则正常返回,评估代码主体中的所有表达式并返回最后一个表达式的值,就像通常一样。那里可能会进行深度递归。通过调用捕获的continuation,可以立即实现退出。
我们在这里看到k被定义了。它是通过call/cc调用来定义的。当调用(call/cc g)时,它会使用当前的延续调用其参数:(g the-current-continuation)the current-continuation是指向call/cc表单返回点的“逃逸过程”。调用它意味着提供一个值,就好像它是由call/cc表单本身返回的一样。
因此,上述结果为:
((lambda(k) (* 5 (k 4))) the-current-continuation) ==>

(* 5 (the-current-continuation 4)) ==>

; to call the-current-continuation means to return the value from
; the call/cc form, so, jump to the return point, and return the value:

4

2

我不会试图解释所有连续性有用的地方,但我希望能够简要介绍我自己经验中发现连续性最常用的主要地方。与其谈论Scheme的call/cc,我会把注意力集中在continuation passing style上。在一些编程语言中,变量可以动态作用域,在没有动态作用域的语言中,可以使用全局变量的样板代码(假设没有多线程代码等问题)。例如,假设有一个当前活动日志流列表*logging-streams*,我们想在动态环境中调用function,其中*logging-streams*logging-stream-x增加。在Common Lisp中,我们可以这样做:

(let ((*logging-streams* (cons logging-stream-x *logging-streams*)))
  (function))

如果我们没有动态作用域变量,就像Scheme一样,我们仍然可以做到

(let ((old-streams *logging-streams*))
  (set! *logging-streams* (cons logging-stream-x *logging-streams*)
  (let ((result (function)))
    (set! *logging-streams* old-streams)
    result))

现在假设我们实际上得到了一个cons-tree,它的非nil叶子是日志流,当调用function时,所有这些日志流都应该在*logging-streams*中。我们有两个选择:
  1. 我们可以展开树,收集所有的日志流,扩展*logging-streams*,然后调用function
  2. 我们可以使用延续传递风格,遍历树,逐渐扩展*logging-streams*,最后在没有更多的tree可遍历时调用function
选项2看起来像这样:
(defparameter *logging-streams* '())

(defun extend-streams (stream-tree continuation)
  (cond
    ;; a null leaf
    ((null stream-tree)
     (funcall continuation))
    ;; a non-null leaf
    ((atom stream-tree)
     (let ((*logging-streams* (cons stream-tree *logging-streams*)))
       (funcall continuation)))
    ;; a cons cell
    (t
     (extend-streams (car stream-tree)
                     #'(lambda ()
                         (extend-streams (cdr stream-tree)
                                         continuation))))))

根据这个定义,我们有

CL-USER> (extend-streams
          '((a b) (c (d e)))
          #'(lambda ()
              (print *logging-streams*)))
=> (E D C B A) 

现在,这个有用吗?在这种情况下,可能没有。一些小的好处可能是extend-streams是尾递归的,所以我们没有很多的堆栈使用,虽然中间闭包弥补了它的堆空间。我们确实有这样一个事实:最终的 continuation 在 extend-streams 设置的任何中间内容的动态范围内执行。在这种情况下,这并不是非常重要,但在其他情况下可能会很重要。
能够抽象掉一些控制流,并具有非局部退出或能够从长时间之前的某个地方继续计算,可以非常方便。例如,在回溯搜索中,这可能很有用。下面是一个基于 continuation passing style 的命题演算求解器,其中一个公式是一个符号(命题文字),或者是一个形如(not formula)(and left right)(or left right)的列表。
(defun fail ()
  '(() () fail))

(defun satisfy (formula 
                &optional 
                (positives '())
                (negatives '())
                (succeed #'(lambda (ps ns retry) `(,ps ,ns ,retry)))
                (retry 'fail))
  ;; succeed is a function of three arguments: a list of positive literals,
  ;; a list of negative literals.  retry is a function of zero
  ;; arguments, and is used to `try again` from the last place that a
  ;; choice was made.
  (if (symbolp formula)
      (if (member formula negatives) 
          (funcall retry)
          (funcall succeed (adjoin formula positives) negatives retry))
      (destructuring-bind (op left &optional right) formula
        (case op
          ((not)
           (satisfy left negatives positives 
                    #'(lambda (negatives positives retry)
                        (funcall succeed positives negatives retry))
                    retry))
          ((and) 
           (satisfy left positives negatives
                    #'(lambda (positives negatives retry)
                        (satisfy right positives negatives succeed retry))
                    retry))
          ((or)
           (satisfy left positives negatives
                    succeed
                    #'(lambda ()
                        (satisfy right positives negatives
                                 succeed retry))))))))

如果找到满足的赋值,则会调用succeed函数,该函数带有三个参数:正文字面量列表、负文字面量列表和可以重试搜索(即尝试查找另一个解决方案)的函数。例如:
CL-USER> (satisfy '(and p (not p)))
(NIL NIL FAIL)
CL-USER> (satisfy '(or p q))
((P) NIL #<CLOSURE (LAMBDA #) {1002B99469}>)
CL-USER> (satisfy '(and (or p q) (and (not p) r)))
((R Q) (P) FAIL)

第二种情况很有趣,因为第三个结果不是“FAIL”,而是一些可调用函数, 它将尝试找到另一个解决方案。在这种情况下,我们可以看到通过使pq为真,可以满足 (or p q)的条件。
CL-USER> (destructuring-bind (ps ns retry) (satisfy '(or p q))
           (declare (ignore ps ns))
           (funcall retry))
((Q) NIL FAIL)

如果我们不使用延续传递风格来保存备选流并稍后返回,那将会非常困难。使用这种方法,我们可以做一些聪明的事情,比如收集所有满足条件的赋值:
(defun satisfy-all (formula &aux (assignments '()) retry)
  (setf retry #'(lambda () 
                  (satisfy formula '() '()
                           #'(lambda (ps ns new-retry)
                               (push (list ps ns) assignments)
                               (setf retry new-retry))
                           'fail)))
  (loop while (not (eq retry 'fail))
     do (funcall retry)
     finally (return assignments)))

CL-USER> (satisfy-all '(or p (or (and q (not r)) (or r s))))
(((S) NIL)   ; make S true
 ((R) NIL)   ; make R true
 ((Q) (R))   ; make Q true and R false
 ((P) NIL))  ; make P true

我们可以略微改变loop并获取仅限于某些n的分配或类似主题的变化。通常情况下,不需要使用继续传递样式,或者可能会使代码难以维护和理解,但在其有用的情况下,它可以使一些本来非常困难的事情变得相对容易。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接