31

对于我的生活,我无法理解延续。我认为问题源于我不明白它们的用途。我在书本或网上找到的所有示例都非常琐碎。他们让我想知道,为什么有人甚至想要延续?

这是一个典型的不切实际的例子,来自TSPL,我相信这是关于该主题的公认书籍。在英语中,他们将延续描述为计算结果的“做什么”。好的,这有点可以理解。

然后,给出的第二个示例:

(call/cc
  (lambda (k)
    (* 5 (k 4)))) => 4 

这有什么意义?k甚至没有定义!(k 4)当甚至无法计算时,如何评估此代码?更何况,怎么call/cc知道把最里面的表达式的参数撕掉4并返回呢?会发生什么(* 5 ..??如果这个最外层的表达式被丢弃,为什么还要写呢?

然后,陈述的一个“不那么”琐碎的例子是如何使用call/cc从递归中提供非本地退出。这听起来像流控制指令,即像break/return命令式语言,而不是计算。

进行这些议案的目的是什么?如果有人需要计算结果,为什么不把它存储起来,以后再根据需要调用。

4

4 回答 4

34

暂时忘掉吧call/cc。任何编程语言中的每个表达式/语句都有一个延续——即你对结果所做的事情。例如,在 C 中,

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

数学作业的延续是printf(...); 的延续(2 * 3)是'加1; 分配给 x; 打印(...)'。从概念上讲,无论您是否可以访问它,延续都在那里。想一想继续需要什么信息——这些信息是 1)堆内存状态(通常),2)堆栈,3)任何寄存器和 4)程序计数器。

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

在 Scheme 和其他一些语言中,您可以访问延续。本质上,在你背后,编译器+运行时将所有需要的信息捆绑在一起,将其存储(通常在堆中)并为你提供处理。你得到的句柄是函数'k'——如果你调用那个函数,你将在该call/cc点之后继续。重要的是,您可以多次调用该函数,并且您将始终在该call/cc点之后继续。

让我们看一些例子:

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

在上面,结果是 3call/cc的结果lambda。没有调用延续。

现在让我们调用延续:

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

通过调用延续,我们在调用之后跳过任何内容并直接在该call/cc点继续。随着12 加到 2(cont 10)的继续返回。10

现在让我们保存延续。

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

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

通常延续用于非本地退出。想想一个函数,它将返回一个列表,除非有什么问题'()会返回。

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

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

       ;; continue processing the list ...

       value-to-return)))
于 2013-05-13T21:07:18.550 回答
9

这是我课堂笔记中的文字:http: //tmp.barzilay.org/cont.txt。它基于许多来源,并且扩展了很多。它有动机、基本解释、关于如何完成的更高级的解释,以及大量从简单到高级的示例,甚至还有一些关于定界延续的快速讨论。

(我试着把整个文本放在这里,但正如我所料,120k 的文本并不能让人如此高兴。

于 2013-05-13T19:27:36.053 回答
5

TL;DR:延续只是捕获的 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 标签体后,我们不能再跳回它。但是在 Scheme 中,有了真正的延续,我们可以。如果我们在由 调用的函数体内设置一些全局变量call/cc,比如(setq qq k)在 Scheme 中,我们可以在以后的任何时间从任何地方调用它,重新进入相​​同的上下文(例如(qq 42))。

关键是,call/cc表单的主体可能包含一个if或一个cond表达式。它只能在某些情况下调用延续,而在其他情况下正常返回,评估代码主体中的所有表达式并像往常一样返回最后一个的值。那里可能会进行深度递归。通过调用捕获的延续,可以实现立即退出。

所以我们看到这里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
于 2013-05-13T21:27:23.500 回答
2

我不会试图解释所有延续可能有用的地方,但我希望我能给出一些简短的例子,说明我在自己的经验中发现延续有用的主要地方。而不是谈论 Scheme 的call/cc,我将注意力集中在continuation pass 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))

现在让我们假设我们实际上得到了一个非nil叶子是日志流的 cons-tree,所有这些都应该在调用*logging-streams*时出现。function我们有两个选择:

  1. 我们可以展平树,收集所有日志流,扩展*logging-streams*,然后调用function
  2. 我们可以使用延续传递风格,遍历树,逐渐扩展,最终在没有更多可遍历的时候*logging-streams*调用。functiontree

选项 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是尾递归,所以我们没有大量的堆栈使用,尽管中间闭包在堆空间中弥补了它。我们确实有这样一个事实,即最终的延续是在任何设置的中间内容的动态范围内执行的。extend-streams在这种情况下,这并不是那么重要,但在其他情况下可能很重要。

能够抽象出一些控制流,并拥有非本地出口,或者能够在不久前的某个地方进行计算,可能非常方便。例如,这在回溯搜索中很有用。这是一个连续传递风格的命题演算求解器,用于公式是符号(命题文字)或形式的列表(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使用三个参数调用 then:正文本列表、负文本列表和可以重试搜索的函数(即尝试找到另一个解决方案)。例如:

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,而是一些可调用的函数,它会尝试找到另一种解决方案。在这种情况下,我们可以看到可以(or p q)通过设置por 或qtrue 来满足:

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 个作业,最多一些n,或该主题的变体。很多时候延续传递风格是不需要的,或者会使代码难以维护和理解,但在它有用的情况下它可以使一些原本非常困难的事情变得相当容易。

于 2013-05-13T21:25:57.210 回答