翻译自: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。