方案延续傻瓜(scheme continuations for dummies)

2019-09-02 23:43发布

对于我的生活,我无法理解的延续。 我认为,从事实,我不明白的是,他们是什么问题造成的。 像我在书中已经找到或网上所有的例子都是很琐碎。 他们让我怀疑,为什么有人甚至想延续?

下面是一个典型的不切实际的例子,从TSPL ,我相信这是相当认可的主题的书。 在英语中,它们所描述的延续为“做什么”与计算的结果。 OK,这就是那种可以理解的。

然后,第二个例子给出:

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

请问这个任何意义? k甚至没有定义! 如何这个代码进行计算,当(k 4)甚至不能被计算的? 更何况,如何call/cc知道参数撕裂了4到最内层的表达和退货吗? 发生什么(* 5 .. ??如果这最外面的表达式被丢弃,为什么还要写呢?

然后,指出一个“少”简单的例子是如何利用call/cc ,以提供从一个递归非本地出口。 这听起来像流控制指令,即像break/return命令式语言中,而不是计算。

什么是通过这些走过场的目的是什么? 如果有人需要计算的结果,为什么不保存它和后来回忆说,根据需要。

Answer 1:

忘记call/cc了一会儿。 每一个表情/声明,任何编程语言,有一个延续 - 这是,你用结果做什么。 在C中,例如,

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

有数学作业存在的延续printf(...) ; 的延续(2 * 3)是“加1; 赋给x; 的printf(...)”。 从概念的延续是有,你是否无法访问它。 想想你需要延续什么样的信息了一会儿 - 的信息是1)堆内存状态(一般),2)堆栈,3)任何寄存器和4)程序计数器。

所以延续存在,但通常他们只是含蓄而无法访问。

在流程,以及一些其他的语言,你有机会获得延续。 从本质上讲,你的背后,编译器+运行时捆绑起来需要一个延续的所有信息,将其存储(一般在堆),并给你一个句柄。 你得到的句柄是函数“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

通过调用延续我们调用后跳过任何东西,在继续正确call/cc点。 与(cont 10)继续返回10被添加到2 12。

现在,让我们保存延续。

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


Answer 2:

这是从我的课堂笔记的文字: http://tmp.barzilay.org/cont.txt 。 它是基于多个来源,并大大延长。 它的动机,基本的解释,它是如何做更高级的解释,而且数量的,从简单到高级的例子,分隔延续,甚至一些快速的讨论。

(我试图把整个文本在这里玩,但如我所料,文字的120K是不是使太高兴了。



Answer 3:

TL; DR:延续只是捕获GOTO语句,以值,更多或更少。

你问的exampe,

(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。 但是,在计划,与真正的延续,我们能做到。 如果我们设置的功能体通过所谓的内部的一些全局变量call/cc ,说(setq qq k)方案我们可以在以后的任何时候调用它,在任何地方,再进入同样的情况下(例如, (qq 42) )。

问题的关键是,主体call/cc形式可以含有ifcond表达。 它可以调用延续只在某些情况下,而在其他正常返回,评估所有表达式的代码体并返回最后一个的值,像往常一样。 可以有很深的递归对那里发生的。 通过调用捕获延续立即出口创汇。

所以我们在这里看到的是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


Answer 4:

我不会试图解释所有的地方延续可能是有用的,但我希望我可以给主要场所的简短的例子,我发现在延续我自己的经验是有用的 。 而不是谈论计划的call/cc ,我把注意力集中在延续传递风格 。 在一些编程语言,变量可以动态范围的,而在没有动态范围的,样板全局变量(假设不存在的多线程代码等问题),语言都可以使用。 举例来说,假设有当前活动日志记录流,列表*logging-streams* ,而且我们要调用function在动态环境*logging-streams*增加有logging-stream-x 在Common Lisp中,我们可以做

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

如果我们没有动态范围的变量,如在方案中,我们仍然可以做

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

现在让我们假设,我们实际上赋予了利弊树,其非nil叶记录流,所有这些都应该是*logging-streams*function被调用。 我们有两个选择:

  1. 我们可以拼合树,收集所有的日志记录流,延长*logging-streams* ,然后调用function
  2. 我们可以使用延续传递风格,遍历树,逐步延长*logging-streams* ,最后调用function时,有没有更多的tree遍历。

选项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被称为三个参数:积极文字的列表,负文字的列表,功能,可以重试搜索(即,试图寻找另一种解决方案)。 例如:

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 pq)是通过使要么可满足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分配,达到某个n,或在该主题的变化。 通常不需要时间延续传递风格,或可以使代码难以维护和理解,但在它有用的情况下,它可以使一些非常否则困难的事情很容易。



文章来源: scheme continuations for dummies