Continuation

2018-02-24 15:53 更新

Continuation

continuation对于编程,就像是达芬奇密码对于人类历史一样:它揭开了人类有史以来最大的谜团。好吧,也许没有那么夸张,不过它们的影响至少和当年发现负数有平方根不相上下。

我们对函数的理解只有一半是正确的,因为这样的理解基于一个错误的假设:函数一定要把其返回值返回给调用者。按照这样的理解,continuation就是更加广义的函数。这里的函数不一定要把返回值传回给调用者,相反,它可以把返回值传给程序中的任意代码。continuation就是一种特别的参数,把这种参数传到函数中,函数就能够根据continuation将返回值传递到程序中的某段代码中。说得很高深,实际上没那么复杂。直接来看看下面的例子好了:

int i = add(5, 10);
int j = square(i);

add这个函数将返回15然后这个值会赋给i,这也是add被调用的地方。接下来i的值又会被用于调用square。请注意支持惰性求值的编译器是不能打乱这段代码执行顺序的,因为第二个函数的执行依赖于第一个函数成功执行并返回结果。这段代码可以用Continuation Pass Style(CPS)技术重写,这样一来add的返回值就不是传给其调用者,而是直接传到square里去了。

int j = add(5, 10, square);

在上例中,add多了一个参数:一个函数,add必须在完成自己的计算后,调用这个函数并把结果传给它。这时square就是add的一个continuation。上面两段程序中j的值都是225。

这样,我们学习到了强制惰性语言顺序执行两个表达式的第一个技巧。再来看看下面IO程序(是不是有点眼熟?):

System.out.println("Please enter your name: ");
System.in.readLine();

这两行代码彼此之间没有依赖关系,因此编译器可以随意的重新安排它们的执行顺序。可是只要用CPS重写它,编译器就必须顺序执行了,因为重写后的代码存在依赖关系了。

 System.out.println("Please enter your name: ", System.in.readLine);

这段新的代码中println需要结合其计算结果调用readLine,然后再返回readLine的返回值。这使得两个函数得以保证按顺序执行而且readLine总被执行(这是由于整个运算需要它的返回值作为最终结果)。Java的println是没有返回值的,但是如果它可以返回一个能被readnLine接受的抽象值,问题就解决了!(译者:别忘了,这里作者一开始就在Java的基础上修改搭建自己的语言)当然,如果一直把函数按照这种方法串下去,代码很快就变得不可读了,可是没有人要求你一定要这样做。可以通过在语言中添加语法糖的方式来解决这个问题,这样程序员只要按照顺序写代码,编译器负责自动把它们串起来就好了。于是就可以任意安排代码的执行顺序而不用担心会失去FP带来的好处了(包括可以用数学方法来分析我们的程序)!如果到这里还有人感到困惑,可以这样理解,函数只是有唯一成员的类的实例而已。试着重写上面两行程序,让println和readLine编程这种类的实例,所有问题就都搞清楚了。
到这里本章基本可以结束了,而我们仅仅了解到continuation的一点皮毛,对它的用途也知之甚少。我们可以用CPS完成整个程序,程序里所有的函数都有一个额外的continuation作为参数接受其他函数的返回值。还可以把任何程序转换为CPS的,需要做的只是把当中的函数看作是特殊的continuation(总是将返回值传给调用者的continuation)就可以了,简单到完全可以由工具自动完成(史上很多编译器就是这样做的)。

一旦将程序转为CPS的风格,有些事情就变得显而易见了:每一条指令都会有一些continuation,都会将它的计算结果传给某一个函数并调用它,在一个普通的程序中这个函数就是该指令被调用并且返回的地方。随便找个之前提到过的代码,比如说add(5,10)好了。如果add属于一个用CPS风格写出的程序,add的continuation很明显就是当它执行结束后要调用的那个函数。可是在一个非CPS的程序中,add的continuation又是什么呢?当然我们还是可以把这段程序转成CPS的,可是有必要这样做吗?
事实上没有必要。注意观察整个CPS转换过程,如果有人尝试要为CPS程序写编译器并且认真思考过就会发现:CPS的程序是不需要栈的!在这里完全没有函数需要做传统意义上的“返回”操作,函数执行完后仅需要接着调用另外一个函数就可以了。于是就不需要在每次调用函数的时候把参数压栈再将它们从中取出,只要把这些参数存放在一片内存中然后使用跳转指令就解决问题了。也完全不需要保留原来的参数:因为这种程序里的函数都不返回,所以它们不会被用第二次!
简单点说呢,用CPS风格写出来的程序不需要栈,但是每次调用函数的时候都会要多加一个参数。非CPS风格的程序不需要额外的参数但又需要栈才能运行。栈里面存的是什么?仅仅是参数还有一个供函数运行结束后返回的程序指针而已。这个时候你是不是已经恍然大悟了?对啊,栈里面的数据实际上就是continuation的信息!栈上的程序返回指针实质上就是CPS程序中需要调用的下一个函数!想要知道add(5, 10)的continuation是什么?只要看它运行时栈的内容就可以了。
接下来就简单多了。continuation和栈上指示函数返回地址的指针其实是同一样东西,只是continuation是显式的传递该地址并且因此代码就不局限于只能返回到函数被调用的地方了。前面说过,continuation就是函数,而在我们特制的语言中函数就是类的实例,那么可以得知栈上指向函数返回地址的指针和continuation的参数是一样的,因为我们所谓的函数(就像类的一个实例)其实就是指针。这也意味着在程序运行的任何时候,你都可以得到当前的continuation(就是栈上的信息)。

好了,我们已经搞清楚当前的continuation是什么了。接下来要弄明白它的存在有什么意义。只要得到了当前的continuation并将它保存起来,就相当于保存了程序的当前状态:在时间轴上把它冻结起来了。这有点像操作系统进入休眠状态。continuation对象保存了足够的信息随时可以从指定的某个状态继续运行程序。在切换线程的时候操作系统也是这样做的。唯一的区别在于它保留了所有的控制权利。当请求某个continuation对象时(在Scheme语言中是通过调用call-with-current-continuation函数实现的)得到的是一个存有当前continuation的对象,也就是栈对象(在CPS中也就是下一个要执行的函数)。可以把这个对象保存做一个变量中(或者是存在磁盘上)。当以该continuation对象“重启”该程序时,程序的状态就会立即“转换”为该对象中保存的状态。这一点和切换回一个被暂停的线程或是从系统休眠中唤醒很相像,唯一不同的是continuatoin对象可以反复的这样使用。当系统唤醒后,休眠前保存的信息就会销毁,否则你也可以反复的从该点唤醒系统,就像乘时光机回到过去一样。有了continuation你就可以做到这一点!

那么continuation在什么情况下有用呢?有一些应用程序天生就没有状态,如果要在这样的系统中模拟出状态以简化工作的时候,就可以用到continuation。最合适的应用场合之一就是网页应用程序。微软的ASP.NET为了让程序员更轻松的编写应用程序,花了大量的精力去模拟各种状态。假如C#支持continuation的话,那么ASP.NET的复杂度将减半:因为只要把某一时刻的continuation保存起来,下次用户再次发起同样请求的时候,重新载入这个continuation即可。对于网络应用的程序员来说就再也没有中断了:轻轻松松程序就从下一行开始继续运行了!对于一些实际问题来说,continuation是一种非常有用的抽象工具。如今大量的传统胖客户端(见瘦客户端)正纷纷走进网络,continuation在未来将扮演越来越重要的角色。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号