我不会试图解释所有延续可能有用的地方,但我希望我能给出一些简短的例子,说明我在自己的经验中发现延续有用的主要地方。而不是谈论 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
我们有两个选择:
- 我们可以展平树,收集所有日志流,扩展
*logging-streams*
,然后调用function
。
- 我们可以使用延续传递风格,遍历树,逐渐扩展,最终在没有更多可遍历的时候
*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
使用三个参数调用 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)
通过设置p
or 或q
true 来满足:
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,或该主题的变体。很多时候延续传递风格是不需要的,或者会使代码难以维护和理解,但在它有用的情况下,它可以使一些原本非常困难的事情变得相当容易。