Lisp 假人的方案延续

Lisp 假人的方案延续,lisp,scheme,continuations,Lisp,Scheme,Continuations,就我的一生而言,我无法理解延续。我认为问题的根源在于我不明白它们的用途。我在书中或网上找到的所有例子都很琐碎。他们让我想知道,为什么有人会想要延续 这是一个典型的不切实际的例子,我相信这是一本公认的关于这个主题的书。在英语中,他们将延拓描述为对计算结果“做什么”。好吧,这是可以理解的 然后,给出第二个示例: (call/cc (lambda (k) (* 5 (k 4)))) => 4 这有什么意义k甚至没有定义!当(k4)甚至无法计算时,如何计算此代码?更不用说,call

就我的一生而言,我无法理解延续。我认为问题的根源在于我不明白它们的用途。我在书中或网上找到的所有例子都很琐碎。他们让我想知道,为什么有人会想要延续

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

然后,给出第二个示例:

(call/cc
  (lambda (k)
    (* 5 (k 4)))) => 4 
这有什么意义<代码>k甚至没有定义!当
(k4)
甚至无法计算时,如何计算此代码?更不用说,
call/cc
如何知道将参数
4
撕到最里面的表达式并返回它?
(*5..
)会发生什么?如果这个最外层的表达式被丢弃,为什么还要编写它


然后,一个“较少”的简单示例是如何使用
call/cc
提供递归的非局部退出。这听起来像是流控制指令,即命令式语言中的
break/return
,而不是计算


做这些动作的目的是什么?如果有人需要计算结果,为什么不存储它,然后根据需要回忆起来呢。

这是我课堂笔记中的文本:。它基于许多来源,并且扩展了很多。它有动机、基本解释、关于如何做的更高级解释,以及大量从简单到高级的示例,甚至一些关于分隔连续体的快速讨论


(我试着把整个文本放在这里,但正如我所预料的,120k的文本并不是那么令人高兴的事。

暂时忘掉
call/cc
。在任何编程语言中,每个表达式/语句都有一个continuation,也就是对结果的处理。例如,在C中

x = (1 + (2 * 3)); 
printf ("Done");
数学作业的续集是
printf(…)
;数学作业的续集是
(2*3)
的续集是'add 1;assign to x;printf(…)'。从概念上讲,续集是存在的,无论您是否有权访问它。请思考一下,续集需要什么信息-信息是1)堆内存状态(通常),2)堆栈,3)任何寄存器和4)程序计数器

所以continuations是存在的,但通常它们只是隐式的,无法访问

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

让我们看一些例子:

> (+ 2 (call/cc (lambda (cont) 3)))
5
在上面的例子中,
call/cc
的结果是
lambda
的结果,它是3。未调用延续

现在让我们调用continuation:

> (+ 2 (call/cc (lambda (cont) (cont 10) 3)))
12
通过调用continuation,我们跳过调用后的任何内容,并在
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
通过保存continuation,我们可以随意使用它来“跳回”在
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)))

我不会试图解释所有延续有用的地方,但我希望我能给出我在自己的经验中发现延续有用的主要地方的简短例子。我不会谈论Scheme的
call/cc
,而是将注意力集中在。在某些编程语言中,变量可以动态范围化,而在没有动态范围化的语言中,可以使用带有全局变量的样板文件(假设不存在多线程代码等问题)。例如,假设有一个当前活动日志流的列表,
*logging streams*
,并且我们希望在动态环境中调用
*logging streams*
,其中
*logging streams*
增加了
logging-stream-x
。在公共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树,它的非
nil
叶子是日志流,当调用
函数
时,所有这些叶子都应该在
*日志流*
中。我们有两个选择:

  • 我们可以展平树,收集所有日志流,扩展
    *日志流*
    ,然后调用
    函数
  • 我们可以使用continuation-passing样式遍历树,逐渐扩展
    *日志流*
    ,当不再有
    要遍历时,最终调用
    函数
  • 选项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
    是尾部递归的,因此我们没有太多的堆栈使用,尽管中间闭包在堆空间中弥补了这一点。我们确实有这样一个事实,即最终的延续是在
    扩展流
    设置的任何中间内容的动态范围内执行的。在这种情况下,这并不是那么重要,但在其他情况下也可以

    能够抽象出一些控制流,并具有非局部的
    (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))))))))
    
    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)
    
    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
    
    (call/cc
      (lambda (k)
        ;;;;;;;;;;;;;;;;
        (* 5 (k 4))                     ;; body of code
        ;;;;;;;;;;;;;;;;
        )) => 4 
    
    (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))
    
    ((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