(car 《为本身写本-Guile-书》)
在本书前言中,我宣称本书的主题是用 Guile 实现一个文式编程工具。接下来,第一章中讲述了如何编写这个文式编程工具的命令行界面,第二章表面上是讲述 Guile 的 I/O 机制,实际上在最后讲述了如何利用 Guile 的 I/O 机制对文式编程元文档进行初步解析,然而本章所讲的东西——续延(Continuation)却与本书主题无必要的关系。原本应该承接第二章的主题写下去的,可是这一章却没这样作,结果形成了读者(我)对后续章节能回到主题上来这样一种期待,我有意无心之间就在现实中创造了一个续延。若是不知道我说什么,能够简单的将这些理解为:本章与本书的主题无关,阅读时能够跳过去。之后须要了,能够再回到这里。回到这里以后,可能又会致使后续章节发生变化。html
setjmp/longjmp
在讲 Guile 的续延以前,先回顾一下 C 语言标准库提供的 setjmp
与 longjmp
这两个函数。看下面的示例:git
#include <setjmp.h> #include <stdio.h> 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); } }
程序输出结果为:github
Entering foo! The result of foo: 1984
这个程序的控制流以下图所示,编程
第一次知道 setjmp
与 longjmp
的存在时,对于已经具有不少 C 语言编程经验的我而言,依然以为很神奇——居然有办法从一个函数的内部直接跳转到另外一个函数的内部。咱们此时此刻在 foo
函数里所做出的决定,居然对已经发生了的事件产生了不可逃避的影响!segmentfault
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 代码中,ui
int i = setjmp(env);
int i =
是一个续延,可将它写为 int i = []
,表示这个赋值过程在等待所赋之值的到来。setjmp
函数第一次被执行后的返回值是 0
,这表示当前的续延 int i = []
调用了 setjmp
函数,获得了值 0
,使得它的计算过程达到终点。spa
后来,在 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-书》)