call with current continuation

翻译自:http://community.schemewiki.org/?call-with-current-continuation

call-with-current-continuation

call-with-current-continuation 简介

Scheme 提供了一个非常强大的控制抽象化叫做 call-with-current-continuation。不仅这个名字令人生畏,连很多对其语义的解释亦是如此。这篇介绍尽量做到平缓易懂。C 程序员也可以阅读这篇 call-with-current-continuation-for-C-programmers。

为了理解这篇文章,你应当对你使用的高阶函数(higher order procedure)清楚掌握,比如 map 和 for-each,lambda 的使用,传递函数给其它函数,以及从函数返回函数。

首先,这篇简介使用缩写来代替这个冗长的名字:call/cc。这些许不再那么吓人并且输入更加方便。一些实现已经提供了一个别名。如果没有提供,为了运行这篇教程里的示例代码可以简单的定义一个:

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

Exit Continuations:让我离开这里

一个最简单的 call/cc 应用就是提供一个简单的方法来离开任何嵌套的计算。有些语言中,比如 Java 和 C,体现为 break,continue 和 return 等关键字。然而现在我有个问题。使用普通的递归,多数需要使用这些语言的这些关键字的问题(比如遍历二维数组)使用递归的方式来解决在Scheme中是十分平常的。所以现在你得容忍我举几个牵强的例子。

让我们以在一个 list 中查找一个条目为例。当然我们可以手动在这个 list 上递归,一旦我们找到了就不再递归了。但是,有 for-each 这个函数来遍历一个 list。出于未知原因,但是我们必须使用它。如果我们像下面这样写,它将会十分强大:

;;; Return the first element in LIST for which WANTED? returns a true 
;;; value. 
;;; NOTE: This is not working Scheme code! 
(define (search wanted? lst) 
  (for-each (lambda (element) 
              (if (wanted? element) 
                  (return element))) 
            lst) 
  #f) 

我们使用了一个叫做 return 的函数,希望从 search 中返回,并且使用当作其参数的 element 的值作为 search 的返回值。如果 for-each 执行完了,我们也没找到被查找的元素,那么返回 #f。

遗憾的是,这样的在Scheme中并不存在。但是,call/cc 可以拯救一切!通过它,我们就可以得到这样的函数。现在,我们该如何使用 call/cc?该函数以另一个函数作为参数,接着作为参数的函数被调用,调用过程中被传入一个单一的参数:该参数正是我们上面想要的 return 函数!当 return 函数被调用时,对 call/cc 的调用立刻返回。特别地,call/cc 返回传递给 return 函数的参数。在实际的例子中,上面的代码可以写成可以真正运行的 Scheme 代码:

;;; Return the first element in LST for which WANTED? returns a true 
;;; value. 
(define (search wanted? lst) 
  (call/cc 
   (lambda (return) 
     (for-each (lambda (element) 
                 (if (wanted? element) 
                     (return element))) 
               lst) 
     #f)))

这里我们可以看到函数 return 是如何被引入到表达式 for-each 的作用域中并被我们调用。当我们找到了想要的元素时,立刻将它传给 return 函数。当以上情况发生时,call/cc 表达式立即返回这个值。其它代码不再执行。整个表达式终止。你可以把这个想象成一种 goto —— 你只是跳出了这些代码和 call/cc 中的调用并且从中返回。

这里有个需要注意的地方。Dijkstra 的著名论文《GOTO陈述有害论》(GOTO Statement Considered Harmful) 在这里也是恰当的:call/cc 能使程序难以理解。应当谨慎限制的使用以及尽量较好地抽象出来,以便它无法干涉对程序的正常理解。

现在我发出这个警告,然后我们继续。你的大脑已经相当乱套了吗?你还没看见 call/cc 能做到的很多事情呢。

既然 return 仅仅是个普通的变量,我们当然可以到处传递它。再一次把例子延伸一点以便聚焦眼前的问题,比如说我们传递的不是一个断言(predicate) wanted?,而是一个函数 treat 接受两个参数:一个元素,一个如果需要这个元素就被调用的函数。

;;; Call LIKE-IT with a custom argument when we like ELEMENT. 
(define (treat element like-it) 
  (if (good-element? element) 
      (like-it 'fnord)))

;;; Call TREAT with every element in the LIST and a procedure to call 
;;; when TREAT likes this element. 
(define (search treat lst) 
  (call/cc 
   (lambda (return) 
     (for-each (lambda (element) 
                 (treat element return)) 
               lst) 
     #f)))

如你所见,我们把从 call/cc 获得的 return 函数传递给 treat。然后 treat 做进一步的处理,如果想要这个元素,就以希望的值作为参数来调用 return 函数。正如我们所知道,一旦调用 return 函数,它的参数就会立刻被生成这个 return 函数的 call/cc 返回。在 search 中,我们有效地从 call/cc 调用的 treat 函数中跳出。

这里与上面 return 的使用没有任何区别,但是你可以看到,它在程序中的任何位置都生效。这个特性赋予了这种操作一个特殊的名字:非本地退出(non-local exit)。我们没有在本地退出代码的地方,而是在一个完全不同的地方或位置退出。

Full Continuations:回来!

目前为止我们只看到了 call/cc 普通的用法。目前为止我们所做的全部就是从一个深度嵌套的调用体系——非常深的函数调用中离开,我们能够使用 return 函数来甩开这些全部的调用回到上层,退出。这叫做 exit continuation。此时,作为 call/cc 的一部分,单词 continuation 又一次出现了。Exit continuation 是一种特殊的 continuation,但是 call/cc 会创建常规的。

Continuation 是一个用来继续(continue)的工具。上面我们使用的 return 是一个 continuation,用来返回到创建它的地方。我们继续了 call/cc 位置的计算。

当 call/cc 产生的 continuation 跳出了传递给 call/cc 的函数的内部的时候会发生什么呢?比如说,下面这个例子:

(define return #f)

(+ 1 (call/cc 
      (lambda (cont) 
        (set! return cont) 
        1)))

这个求值程序将输出 2,因为 call/cc 表达式的主体正常退出,返回了 1。然后与 1 相加,产出 2。但是!现在我们有一个叫做 return 的变量,它被绑定到了 call/cc 产生的 continuation 上。如果我们传递一个参数,比如 22,来调用它,会发生什么呢?

> (return 22)
23

现在这里发生了什么呢?函数 return 与它上面做的事情相同:传递给 return 的 22 被用来当做调用 call/cc 的返回值。然后这个返回值与 1 相加,产出 23。这就是我们得到的。我们从来没有从调用 return 的地方返回,而是在上面的另外一个位置返回了。

这里不同的是我们从外界重新进入了这个计算过程,而不只是仅仅离开它。这是一个超大的脑筋急转弯。千万要理解它!

Coroutines:出去,进来,出去...

假设我们有个函数执行一项耗时的任务。我们不想完全阻塞住系统,所以我们把这个任务分割成小步骤。当我们完成其中一个步骤时,我们调用一个函数,让系统执行任意一个现在想要执行的任务。拧劲儿的地方来了:这个函数接受一个函数,具体来说是个 continuation,被用来调用以继续这个耗时的计算:

(define (hefty-computation do-other-stuff) 
  (let loop ((n 5)) 
    (display "Hefty computation: ") 
    (display n) 
    (newline) 
    (set! do-other-stuff (call/cc do-other-stuff)) 
    (display "Hefty computation (b)")  
    (newline) 
    (set! do-other-stuff (call/cc do-other-stuff)) 
    (display "Hefty computation (c)") 
    (newline) 
    (set! do-other-stuff (call/cc do-other-stuff)) 
    (if (> n 0) 
        (loop (- n 1))))) 

当 do-other-stuff 调用 call/cc 传递给它当做参数的函数的时候,计算将会在这里继续,循环(loop)将接着递归,下一步被执行。当然,除了这种大型计算,所有其它的计算都是多余的。既然是多余的,它应当做相同的事情,点到为止,可能的话就让系统处理其它的事物:

;; notionally displays a clock 
(define (superfluous-computation do-other-stuff) 
  (let loop () 
    (for-each (lambda (graphic) 
                (display graphic) 
                (newline) 
                (set! do-other-stuff (call/cc do-other-stuff))) 
              '("Straight up." "Quarter after." "Half past."  "Quarter til.")) 
    (loop))) 

如果你照着下面输入就会出现这些:

> (hefty-computation superfluous-computation) 
Hefty computation: 5 
Straight up. 
Hefty computation (b) 
Quarter after. 
Hefty computation (c) 
Half past. 
Hefty computation: 4 
Quarter til. 
. 
. 
. 

现在这里发生了什么呢?这个大型计算执行一步,就把控制权交给其它多余的计算。同样执行一步之后就把控制权还给大型计算。现在当然又是执行一步之后向其它多余计算交出控制权。然后……你发现了这个模式。这两个函数相互调用,在这两个之间传递控制权。当然,这里没有限制为两个函数,而是能够在任意数量函数之间实现。像这样在它们之间传递控制权的函数叫做协程(Coroutine),因此实际上它们是并行的。

我希望现在你对协程有了一个基础的认识,并且能够告诉其他对此畏惧的人说这一点也不吓人。但是让我重复我先前的警告:Continuation 能够被轻易的滥用。不要过度使用它。在任何地方你都不是必须使用 continuation 的,真的!


你还可以参考 R5RS 中的 call-with-current-continuation。

你可能感兴趣的:(call with current continuation)