跳转到内容

Scheme 编程/连续

来自维基教科书,开放的书籍,开放的世界

call-with-current-continuation 简介

[编辑 | 编辑源代码]

call-with-current-continuation是一个过程,它接受另一个过程作为参数,该过程本身接受一个参数。该参数是一个连续,用 Scheme 发明者 Gerald Sussman 和 Guy Steel 的话来说,是“即将从它返回”。call-with-current-continuation. 连续是一个过程,它代表“计算的剩余部分”。当你将连续作为一个过程调用时,控制跳转到call-with-current-continuation最初被调用,并且你提供给连续的任何参数都成为对该调用的返回值call-with-current-continuation.

保存以下程序

cont-test.scm

(define continuations '())

(define (push arg)
  (set! continuations
	(cons arg continuations)))

(define (capture-from-map arg)
  (call-with-current-continuation
   (lambda (cc)
     (push cc)
     arg)))

然后,从 REPL 中尝试以下操作

(load "cont-test.scm")
;  loading cont-test.scm
;  done loading cont-test.scm
#<unspecified>
> (define numbers (map capture-from-map '(1 2 3 4 5 6)))
#<unspecified>
> numbers
(1 2 3 4 5 6)
> continuations
(#<continuation 186 @ 891c400> #<continuation 186 @ 891c800> #<continuation 186 @ 891cc00> #<continuation 186 @ 891c000> #<continuation 186 @ 891b400> #<continuation 186 @ 891b800>)
> ((car (reverse continuations)) 76)
#<unspecified>
> numbers
(76 2 3 4 5 6)

这里发生了什么?在评估(define numbers (map capture-from-map '(1 2 3 4 5 6)))后,连续变量包含每次map调用capture-from-map时的连续,按逆序排列。所以当我们评估((car (reverse continuations) 76)时,执行控制跳转回capture-from-map最初返回1map函数的位置,并且在该时间点存在的整个程序堆栈都恢复了。从那时起“计算的剩余部分”是map映射capture-from-map到列表的剩余部分,即(2 3 4 5 6),然后将生成的列表定义为变量numbers. 但是这次,我们改变了在call-with-current-continuationlambda

内部进行的那部分计算的结果。numbers(注意:存储在map中的结果依赖于实现,因为标准没有指定

调用作为参数传递的过程的顺序。连续变量在所有这一切的范围之外,并且没有恢复为'()当控制跳转回来时。结果,五个新的连续被添加到连续:

> continuations
(#<continuation 186 @ 8923c00> #<continuation 186 @ 8923000> #<continuation 186 @ 8922400> #<continuation 186 @ 8922800> #<continuation 186 @ 8922c00> #<continuation 186 @ 8922000> #<continuation 186 @ 8921400> #<continuation 186 @ 8921800> #<continuation 186 @ 8921c00> #<continuation 186 @ 8921000> #<continuation 186 @ 891f400>)
> (length continuations)
11
>

在某些 Scheme 实现中,call-with-current-continuation也定义为call/cc. 然而,SCM 没有定义这种较短的形式。如果你需要,你可以自己定义它

(define call/cc call-with-current-continuation)

使用连续实现类似 C++ 的异常

[编辑 | 编辑源代码]

一些 Scheme 实现有自己的错误处理语法,但 SCM 没有。我们可以使用连续和宏的组合定义一个异常系统。C++ 异常是动态的:当你抛出一个异常时,控制跳转到下一个catch块,一直到调用堆栈的顶端。为了在我们的实现中实现这一点,我们将保留一个连续列表,这些连续代表catch块。每次一个try块被进入时,它会将一个连续添加到这个堆栈中,每次一个try块退出时,一个连续就会从堆栈中移除。最简单的部分是实现连续堆栈

(define *cont-stack* '())

(define (push-catch cont)
  (set! *cont-stack* (cons cont *cont-stack*)))

(define (pop-catch)
  (let ((retval (car *cont-stack*)))
    (set! *cont-stack* (cdr *cont-stack*))
    retval))

每次我们调用push-catch时,一个新的连续都会被添加到堆栈中。当我们调用pop-catch时,我们会得到最后一个被添加的连续(然后是再上一个连续,依此类推),并且该连续也会从堆栈中移除。

dynamic-wind

[编辑 | 编辑源代码]

困难的部分是确保我们检测到块每次退出或重新进入(这可能会因为连续发生),这样我们才能将连续堆栈保持在正确状态。幸运的是,Scheme 提供了dynamic-wind,它接受三个过程作为参数,并且与中间 lambda 具有相同的返回值。第一个和最后一个过程是保护过程,它们可以在进入中间过程之前和退出中间过程之后执行初始化和清理

(dynamic-wind
   (lambda ()
      (let ((exception
	     (call-with-current-continuation
	      (lambda (cc)
		(push-catch cc)
		#f))))
	(if exception
	    (begin catch-body ... (escape)))))
    (lambda ()
      (begin body ...))
    (lambda () (pop-catch)))

唯一剩下的就是定义escape连续,它允许执行在try块执行之后离开catch块(否则body将再次执行),并将所有内容包装在一个宏中。整个程序如下所示

exception.scm

(require 'macro) ; Required to use this with SCM. Other Scheme implementations
                 ; may not require this, or it may even be an error.
(define *cont-stack* '())

(define (push-catch cont)
  (set! *cont-stack* (cons cont *cont-stack*)))

(define (pop-catch)
  (let ((retval (car *cont-stack*)))
    (set! *cont-stack* (cdr *cont-stack*))
    retval))

(define (throw exn)
  (if (null? *cont-stack*)
      (error "Can't use throw outside of a try form")
      ((car *cont-stack*) exn)))

(define-syntax try
  (syntax-rules (catch)
    ((try (body ...)
	  catch exception (catch-body ...))
     (call-with-current-continuation
      (lambda (escape)
	(dynamic-wind
	    (lambda ()
	      (let ((exception
		     (call-with-current-continuation
		      (lambda (cc)
			(push-catch cc)
			#f))))
		(if exception
		    (begin catch-body ... (escape)))))
	    (lambda ()
	      (begin body ...))
	    (lambda () (pop-catch))))))))

将以上程序加载到 Scheme 中后,你就可以在一个地方放置错误处理代码,并且可以通过调用throw过程来跳转到该位置,该过程的参数可以是任何值,但#f除外。使用参数将有关发生错误的信息发送给错误处理代码。

(define (my-/ numerator denominator)
  (if (not (and (number? numerator)
                (number? denominator)))
      (throw "my-/ is for numbers")
      (/ numerator denominator)))

(try ((display (my-/ "two" "one"))
      (newline))
 catch message ((display message)
                (newline)))
华夏公益教科书