简体   繁体   English

计划延续假人

[英]scheme continuations for dummies

For the life of me, I can't understand continuations. 对于我的生活,我无法理解延续。 I think the problem stems from the fact that I don't understand is what they are for . 我认为这个问题源于我不明白它们的用途 All the examples that I've found in books or online are very trivial. 我在书籍或网上找到的所有例子都非常简单。 They make me wonder, why anyone would even want continuations? 他们让我想知道,为什么有人甚至想要延续?

Here's a typical impractical example, from TSPL , which I believe is quite recognized book on the subject. 这是一个典型的不切实际的例子,来自TSPL ,我认为这是一本非常公认的关于这个主题的书。 In english, they describe the continuation as "what to do" with the result of a computation. 在英语中,他们将继续描述为计算结果的“做什么”。 OK, that's sort of understandable. 好的,这是可以理解的。

Then, the second example given: 然后,第二个例子给出:

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

How does this make any sense?? 这有什么用? k isn't even defined! k甚至没有定义! How can this code be evaluated, when (k 4) can't even be computed? (k 4)甚至无法计算时,如何评估此代码? Not to mention, how does call/cc know to rip out the argument 4 to the inner most expression and return it? 更不用说, call/cc如何知道将参数4删除到最内层表达式并返回它? What happens to (* 5 .. ?? If this outermost expression is discarded, why even write it? 会发生什么(* 5 .. ??如果这个最外层的表达式被丢弃,为什么要写呢?

Then, a "less" trivial example stated is how to use call/cc to provide a nonlocal exit from a recursion. 然后,一个“较少”的简单示例说明如何使用call/cc来提供递归的非本地退出。 That sounds like flow control directive, ie like break/return in an imperative language, and not a computation. 这听起来像流控制指令,即像命令式语言中的break/return ,而不是计算。

And what is the purpose of going through these motions? 通过这些动议的目的是什么? If somebody needs the result of computation, why not just store it and recall later, as needed. 如果有人需要计算结果,为什么不只是存储它并在以后根据需要调用。

Forget about call/cc for a moment. 暂时忘掉call/cc Every expression/statement, in any programming language, has a continuation - which is, what you do with the result. 在任何编程语言中,每个表达式/语句都有一个延续 - 也就是说,你对结果做了什么。 In C, for example, 以C为例,

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

has the continuation of the math assignment being printf(...) ; 将数学作业的继续作为printf(...) ; the continuation of (2 * 3) is 'add 1; (2 * 3)的延续是'加1; assign to x; 分配给x; printf(...)'. 的printf(...)”。 Conceptually the continuation is there whether or not you have access to it. 从概念上讲,无论您是否有权访问它,都会继续存在。 Think for a moment what information you need for the continuation - the information is 1) the heap memory state (in general), 2) the stack, 3) any registers and 4) the program counter. 想一想继续所需的信息 - 信息是1)堆内存状态(一般),2)堆栈,3)任何寄存器和4)程序计数器。

So continuations exist but usually they are only implicit and can't be accessed. 因此存在延续,但通常它们只是隐含的,不能被访问。

In Scheme, and a few other languages, you have access to the continuation. 在Scheme和其他一些语言中,您可以访问continuation。 Essentially, behind your back, the compiler+runtime bundles up all the information needed for a continuation, stores it (generally in the heap) and gives you a handle to it. 基本上,在您的背后,编译器+运行时捆绑了延续所需的所有信息,将其存储(通常在堆中)并为您提供句柄。 The handle you get is the function 'k' - if you call that function you will continue exactly after the call/cc point. 你得到的句柄是函数'k' - 如果你调用该函数,你将在call/cc点之后继续。 Importantly, you can call that function multiple times and you will always continue after the call/cc point. 重要的是,您可以多次调用该函数,并且您将始终在call/cc点之后继续。

Let's look at some examples: 我们来看一些例子:

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

In the above, the result of call/cc is the result of the lambda which is 3. The continuation wasn't invoked. 在上面, call/cc的结果是lambda为3的结果。没有调用continuation。

Now let's invoke the continuation: 现在让我们调用延续:

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

By invoking the continuation we skip anything after the invocation and continue right at the call/cc point. 通过调用continuation,我们在调用后跳过任何内容,并在call/cc点继续。 With (cont 10) the continuation returns 10 which is added to 2 for 12. 使用(cont 10) ,继续返回10 ,将其加到2为12。

Now let's save the continuation. 现在让我们保存延续。

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

By saving the continuation we can use it as we please to 'jump back to' whatever computation followed the call/cc point. 通过保存延续,我们可以随意使用它来“跳回” call/cc点之后的任何计算。

Often continuations are used for a non-local exit. 通常延续用于非本地退出。 Think of a function that is going to return a list unless there is some problem at which point '() will be returned. 想想一个将返回列表的函数,除非在返回'()时出现问题。

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

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

       ;; continue processing the list ...

       value-to-return)))

Here is text from my class notes: http://tmp.barzilay.org/cont.txt . 以下是我班级笔记中的文字: http//tmp.barzilay.org/cont.txt It is based on a number of sources, and is much extended. 它基于许多来源,并且得到了很大的扩展。 It has motivations, basic explanations, more advanced explanations for how it's done, and a good number of examples that go from simple to advanced, and even some quick discussion of delimited continuations. 它有动机,基本解释,对它如何完成的更高级解释,以及从简单到高级的大量示例,甚至是对分隔延续的一些快速讨论。

(I tried to play with putting the whole text here, but as I expected, 120k of text is not something that makes SO happy. (我尝试将整篇文章放在这里,但正如我所料,120k的文字并不能令人高兴。

TL;DR : continuations are just captured GOTOs, with values, more or less. TL; DR :延续只是捕获GOTO,值或多或少。

The exampe you ask about, 你问的例子,

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

can be approximately translated into eg Common Lisp, as 可以大致翻译成例如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))

This is just an illustration; 这只是一个例子; in Common Lisp we can't jump back into the PROG tagbody after we've exited it the first time. 在Common Lisp中,我们第一次退出后,我们无法跳回到PROG标签中。 But in Scheme, with real continuations, we can. 但是在Scheme中,我们可以通过实际的延续。 If we set some global variable inside the body of function called by call/cc , say (setq qq k) , in Scheme we can call it at any later time, from anywhere, re-entering into the same context (eg (qq 42) ). 如果我们在call/cc的函数体内设置一些全局变量,比如说(setq qq k) ,在Scheme中我们可以在任何时候,从任何地方调用它,重新进入相​​同的上下文(例如(qq 42) )。

The point is, the body of call/cc form may contain an if or a cond expression. 关键是, call/cc形式的主体可能包含ifcond表达式。 It can call the continuation only in some cases, and in others return normally, evaluating all expressions in the body of code and returning the last one's value, as usual. 它只能在某些情况下调用continuation,而在其他情况下,通常会返回代码体中的所有表达式,并像往常一样返回最后一个值。 There can be deep recursion going on there. 那里可能会有很深的递归。 By calling the captured continuation an immediate exit is achieved. 通过调用捕获的延续,可以立即退出。

So we see here that k is defined. 所以我们在这里看到k 定义的。 It is defined by the call/cc call. 它由call/cc调用定义。 When (call/cc g) is called, it calls its argument with the current continuation: (g the-current-continuation) . (call/cc g) ,它用当前的continuation调用它的参数:( (g the-current-continuation) the current-continuation is an "escape procedure" pointing at the return point of the call/cc form. the current-continuation一个指向call/cc表单返回点的“转义过程” To call it means to supply a value as if it were returned by the call/cc form itself. 调用它意味着提供一个值,就好像它是由call/cc表单本身返回的一样。

So the above results in 所以上面的结果

((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

I won't try to explain all the places where continuations can be useful, but I hope that I can give brief examples of main place where I have found continuations useful in my own experience. 我不会试图解释延续可能有用的所有地方,但我希望我能给出一些主要地方的简短例子,我发现在我自己的经验中有用的延续。 Rather than speaking about Scheme's call/cc , I'd focus attention on continuation passing style . 我不是在谈论Scheme的call/cc ,而是将注意力集中在延续传递方式上 In some programming languages, variables can be dynamically scoped, and in languages without dynamically scoped, boilerplate with global variables (assuming that there are no issues of multi-threaded code, etc.) can be used. 在一些编程语言中,变量可以是动态范围的,并且在没有动态范围的语言中,可以使用具有全局变量的样板(假设不存在多线程代码等问题)。 For instance, suppose there is a list of currently active logging streams, *logging-streams* , and that we want to call function in a dynamic environment where *logging-streams* is augmented with logging-stream-x . 例如,假设有一个当前活动的日志流列表, *logging-streams* ,我们希望在动态环境中调用function ,其中*logging-streams*使用logging-stream-x扩充。 In Common Lisp we can do 在Common Lisp中我们可以做到

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

If we don't have dynamically scoped variables, as in Scheme, we can still do 如果我们没有动态范围的变量,就像在Scheme中一样,我们仍然可以

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

Now lets assume that we're actually given a cons-tree whose non- nil leaves are logging-streams, all of which should be in *logging-streams* when function is called. 现在让我们假设,我们实际上赋予了利弊树,其非nil叶记录流,所有这些都应该是*logging-streams*function被调用。 We've got two options: 我们有两个选择:

  1. We can flatten the tree, collect all the logging streams, extend *logging-streams* , and then call function . 我们可以展平树,收集所有日志流,扩展*logging-streams* ,然后调用function
  2. We can, using continuation passing style, traverse the tree, gradually extending *logging-streams* , finally calling function when there is no more tree to traverse. 我们可以使用延续传递样式遍历树,逐渐扩展*logging-streams* ,最后在没有更多tree遍历的情况下调用function

Option 2 looks something like 选项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))))))

With this definition, we have 有了这个定义,我们就有了

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

Now, was there anything useful about this? 现在,有什么有用的吗? In this case, probably not. 在这种情况下,可能不是。 Some minor benefits might be that extend-streams is tail-recursive, so we don't have a lot of stack usage, though the intermediate closures make up for it in heap space. 一些小的好处可能是extend-streams是尾递归的,所以我们没有大量的堆栈使用,尽管中间闭包在堆空间中弥补了它。 We do have the fact that the eventual continuation is executed in the dynamic scope of any intermediate stuff that extend-streams set up. 我们确实有这样一个事实,即最终的延续是在extend-streams设置的任何中间内容的动态范围内执行的。 In this case, that's not all that important, but in other cases it can be. 在这种情况下,这并不是那么重要,但在其他情况下它可以。

Being able to abstract away some of the control flow, and to have non-local exits, or to be able to pick up a computation somewhere from a while back, can be very handy. 能够抽象出一些控制流,并且具有非本地出口,或者能够从一段时间内某处获取计算,可以非常方便。 This can be useful in backtracking search, for instance. 例如,这在回溯搜索中非常有用。 Here's a continuation passing style propositional calculus solver for formulas where a formula is a symbol (a propositional literal), or a list of the form (not formula) , (and left right) , or (or left right) . 这里是公式的延续传递方式命题演算求解器,其中公式是符号(命题文字),或形式列表(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))))))))

If a satisfying assignment is found, then succeed is called with three arguments: the list of positive literals, the list of negative literals, and function that can retry the search (ie, attempt to find another solution). 如果找到一个满意的分配,然后succeed被称为三个参数:积极文字的列表,负文字的列表,功能,可以重试搜索(即,试图寻找另一种解决方案)。 For instance: 例如:

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)

The second case is interesting, in that the third result is not FAIL , but some callable function that will try to find another solution. 第二种情况很有意思,因为第三种结果不是FAIL ,而是一些可调用的函数,它将尝试找到另一种解决方案。 In this case, we can see that (or pq) is satisfiable by making either p or q true: 在这种情况下,通过使pq真,我们可以看到(or pq)是可满足的:

CL-USER> (destructuring-bind (ps ns retry) (satisfy '(or p q))
           (declare (ignore ps ns))
           (funcall retry))
((Q) NIL FAIL)

That would have been very difficult to do if we weren't using a continuation passing style where we can save the alternative flow and come back to it later. 如果我们没有使用延续传递方式,我们可以保存替代流程并在以后再回到它,那将是非常困难的。 Using this, we can do some clever things, like collect all the satisfying assignments: 使用这个,我们可以做一些聪明的事情,比如收集所有令人满意的任务:

(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

We could change the loop a bit and get just n assignments, up to some n , or variations on that theme. 我们可以稍微改变loop并获得n个赋值,最多n个或该主题的变体。 Often times continuation passing style is not needed, or can make code hard to maintain and understand, but in the cases where it is useful, it can make some otherwise very difficult things fairly easy. 通常不需要时间延续传递风格,或可以使代码难以维护和理解,但在它有用的情况下,它可以使一些非常否则困难的事情很容易。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM