(car 《为自己写本-Guile-书》)
在本书前言中,我宣称本书的主题是用 Guile 实现一个文式编程工具。接下来,第一章中讲述了如何编写这个文式编程工具的命令行界面,第二章表面上是讲述 Guile 的 I/O 机制,实际上在最后讲述了如何利用 Guile 的 I/O 机制对文式编程元文档进行初步解析,然而本章所讲的东西——续延(Continuation)却与本书主题无必要的关系。本来应该承接第二章的主题写下去的,但是这一章却没这样做,结果造成了读者(我)对后续章节能回到主题上来这样一种期待,我有意无意之间就在现实中创造了一个续延。如果不知道我说什么,可以简单的将这些理解为:本章与本书的主题无关,阅读时可以跳过去。以后需要了,可以再回到这里。回到这里之后,可能又会导致后续章节发生变化。
setjmp/longjmp
在讲 Guile 的续延之前,先回顾一下 C 语言标准库提供的 setjmp
与 longjmp
这两个函数。看下面的示例:
#include
#include
jmp_buf env;
void
foo(void) {
printf("Entering foo!\n");
longjmp(env, 1984);
printf("Exiting foo!\n");
}
int
main(void) {
int i = setjmp(env);
if (i == 0) {
foo();
} else {
printf("The result of foo: %d\n", i);
}
}
程序输出结果为:
Entering foo!
The result of foo: 1984
这个程序的控制流如下图所示,
第一次知道 setjmp
与 longjmp
的存在时,对于已经具备很多 C 语言编程经验的我而言,依然觉得很神奇——竟然有办法从一个函数的内部直接跳转到另一个函数的内部。我们此时此刻在 foo
函数里所作出的决定,竟然对已经发生了的事件产生了不可逃避的影响!
call/cc
以下 Guile 代码与上一节的 C 代码近乎等效:
(define cc 'current-continuation)
(define (foo)
(display "Entering foo!\n")
(cc 1984)
(display "Exiting foo!\n"))
(let ((i (call/cc (lambda (k)
(set! cc k)
(k 0)))))
(cond ((= i 0) (foo))
(else (begin
(display "The result of foo: ")
(display i)
(newline)))))
上述 Guile 代码与上一节 C 代码的对应关系如下:
-
call/cc
类似于setjmp
; - 全局变量
(cc 1984)
类似于全局变量env
与longjmp
的『合体』——longjmp(env, 1984)
。
续延
在下面的这行 C 代码中,
int i = setjmp(env);
int i =
是一个续延,可将它写为 int i = []
,表示这个赋值过程在等待所赋之值的到来。setjmp
函数第一次被执行后的返回值是 0
,这表示当前的续延 int i = []
调用了 setjmp
函数,得到了值 0
,使得它的计算过程达到终点。
后来,在 foo
函数中执行了 longjmp(env, 1984)
,导致程序的执行点又跳到上一行代码中的 setjmp(env)
位置,将 longjmp
的参数值 1984
传递给 setjmp
函数,然后第二次执行 setjmp
函数,让它返回 1984
,于是就完成了对 i
的第二次赋值。可以将这个过程想象为,我们将 int i = []
这个续延保存到了 env
这个全局变量中,然后在其他地方可以通过 longjmp
让这个续延再次得到所赋之值。
将一个计算过程中的某个计算单元『抽走』,这就制造了一个续延。无论何时,只要重新补上缺失的计算单元,这个计算过程会基于所填补的计算单元产生相应的结果。这没有什么高深莫测的东西,在生活中我们经常运用续延这种技巧。譬如,考试时,遇到不会做的题目,可以暂时跳过去——大不了不挣这些题目的分,等把后面的题目都完成了,再回头跟它们慢慢死磕。
续延在等候它所缺失的计算单元,这种行为类似于函数们在等候参数值的传入。如果向续延提供了它所缺失的计算单元,续延就会将这个计算单元映射为续延所对应的计算过程的最终计算结果。如果向函数提供了参数值,函数会将这些参数值映射会函数的返回值。所以,在行为上续延与函数是等价的,所以可将其视为一种另类的函数。
简单的说,续延就是在表达式上挖了个洞,让它变成了一种类似函数的东西。
call-with-current-continuation
call/cc
是 call-with-current-continuation
的简写,意思是『用当前的续延来调用』。来调用什么?一个匿名函数:
(lambda (k)
(set! cc k)
(k 0))
这个匿名函数的形参 k
是一个续延。call/cc
会捕捉当前的续延,将它作为参数传递给这个匿名函数,即调用这个匿名函数。
对于上一节的 Guile 代码而言,call/cc
捕捉的当前续延是:
(let ((i []))
(cond ((= i 0) (foo))
(else (begin
(display "The result of foo: ")
(display i)
(newline)))))
假设这个续延为 i-赋值续延
,它会被 call/cc
作为参数传递给上述的匿名函数:
((lambda (k)
(set! cc k)
(k 0)) i-赋值续延)
这个匿名函数接受这个续延后,会执行以下两个运算过程:
(set! cc i-赋值续延)
(i-赋值续延 0)
第一个运算过程是用全局变量 cc
记录这个续延。第二个计算过程是以参数值 0
『调用』这个续延——参数值 0
恰好填补了 i-赋值续延
所缺失的计算单元,结果 i
被绑定到 0
上,使得续延变为:
(let ((i 0))
(cond ((= i 0) (foo))
(else (begin
(display "The result of foo: ")
(display i)
(newline)))))
接下来,cond
的第一个谓词 (= 1 0)
的结果为真,于是进入 foo
函数的计算过程,结果会遇到 (cc 1984)
。由于在 call/cc
语句中,已将 i-赋值续延
记录于 cc
。因此 (cc 1984)
本质上就是用 1984
来填补 i-赋值续延
所缺失的计算单元,将其变为:
(let ((i 1984))
(cond ((= i 0) (foo))
(else (begin
(display "The result of foo: ")
(display i)
(newline)))))
现在 i
的值就变成了 1984
了,因此接下来 cond
的第一个谓词的结果为假,从而进入 else
分支。最终得到以下结果:
Entering foo!
The result of foo: 1984
注意,在 foo
函数中,当 (cc 1984)
语句被执行时,本质上它会将当前的程序环境切换到 i-赋值续延
环境,因此位于它后面的 (display "Exiting foo!\n")
语句不会有运行机会。
它有什么用?
十三年前,王垠写过一篇文章『二叉树匹配问题』,较为详细的诠释了《Teach Yourself Scheme in Fixnum Days》这本书的第十三章中的一个续延示例。可以结合这两份文档了解一下续延的应用场合。这个二叉树匹配问题是基于续延构造了一个二叉树结点生成器来解决的。很坦诚的说,这两份文档所讲的东西,目前我也只是似懂非懂。也许只有在真正需要使用续延的时候,方能真正知道怎么运用它。我觉得在现实中续延真正有用的地方就在于实现协程。不过《Teach Yourself Scheme in Fixnum Days》第十四章、十五章对续延有着更有趣的应用——非确定性运算与引擎。
(cdr 《为自己写本-Guile-书》)