控制结构(11): Continuation passing style(CPS)
// 上一篇:控制结构(10)指令序列(opcode)
[注释]:
- 这个笔记系列需要告一个段落了,收尾部分整理下几个时髦(The New Old Things)结构。
- 后面打算开一个算法方面的,重新学习下算法方面的知识。
前面的文章里,在所有异步的地方都只假设通过callback的方式实现,而不用其他新的原子结构。实际上,如果我们进一步要求:
- 不使用return函数返回
- 一个函数执行的最后一步要么退出,要么调用其他函数来传递结果。
那么,这样的一个函数在最后一个动作之后是不需要保存Stack的。如果这个接收结果的被调函数本身总是以函数参数的形式传递,则我们称这样的手法为“CPS”:
In functional programming, continuation-passing style (CPS) is a style of programming in which control is passed explicitly in the form of a continuation. This is contrasted with direct style, which is the usual style of programming. --wiki:CPS
wiki里面举例以lisp函数语言的case居多,对于习惯命令式语言的人来说,阅读起来可能感觉不是很强烈。以JavaScript的视角,可以阅读下面这三篇文章:
- https://blogs.msdn.microsoft.com/ericlippert/2005/08/08/recursion-part-four-continuation-passing-style/
- https://blogs.msdn.microsoft.com/ericlippert/2005/08/11/recursion-part-five-more-on-cps/
- https://blogs.msdn.microsoft.com/ericlippert/2005/08/15/recursion-part-six-making-cps-work/
以下翻译上述链接里面的例子.
假设有这样一个命令式的递归函数:
function calc(l,r){
return l+r;
}
function vist(tree){
if(tree==null){
return 0;
}else{
var l = vist(tree.left);
var r = vist(tree.right);
return calc(l,r);
}
}
我们希望一步步把它改写成CPS风格的代码,第一步把visit函数的返回去掉,改用一个回调函数k来接收结果:
function vist(tree, k){
if(tree==null){
k(0);
}else{
// var l = vist(tree.left);
// var r = visit(tree.right);
k(calc(l,r));
}
}
注意到上面的代码注释了两行,因为我们希望visit没有返回值,则这两行代码不能再调用visit的原版代码。怎么做呢?
通过反复执行下面的步骤来达成:
- 被注释掉的部分,最后一个操作的结果应该传递给一个“回调函数”,假设是C
- 使用了该结果的后续代码应该被“卷入”到C里面。
于是上述代码变为:
function visit(tree,k){
if(tree==null){
k(0);
}else{
// var l = vist(tree.left);
// 这个callback消灭了var r = visit(tree.right);和k(calc(l,r));
function kRight(r){
k(calc(l,r));
}
// 此时可以把kRight作为visit的第2个参数,这是一个CPS
visit(tree.right, kRight);
}
}
如此,我们还差一个// var l = vist(tree.left);
没有消灭,再来一次:
function visit(tree, k){
if(tree==null){
k(0);
}else{
function kLeft(l){
function kRight(r){
k(calc(l,r));
}
visit(tree.right, kRight);
}
visit(tree.left, kLeft);
}
}
除了calc那个地方依然是命令式代码,其他部分都已经是CPS风格代码。但这个地方的消除就留给读者做一个练习。当一个代码全部都变成函数调用的时候,我们可以让每个函数调用的地方实际上都变成生成包含发生这个函数调用所需要的数据,而不是直接执行它
。但在此之前,我们先让上面函数的参数合并成一个(一个简单的结构体就可以搞定它)。
function visit(args){
if(args.tree==null){
args.k(0);
}else{
function kLeft(l){
function kRight(r){
args.k(calc(l,r));
}
visit(args.tree.right, kRight);
}
visit(args.tree.left, kLeft);
}
}
好了,现在,我们先造一个生成包含发生这个函数调用所需要的数据
的辅助函数:
var continuation = null;
function createContinuation(newFun,newArgs){
continuation = {func: newFun, args: newArgs};
}
那么,visit函数就会被改写成一个让每个函数调用的地方实际上都变成生成包含发生这个函数调用所需要的数据,而不是直接执行它
的函数:
function visit(args){
if(args.tree==null){
createContinuation(k, 0);
}else{
function kLeft(l){
function kRight(r){
//args.k(calc(l,r));
createContinuation(k, calc(l,r));
}
//visit(args.tree.right, kRight);
createContinuation(visit, {tree:args.tree.right,k:kRight});
}
//visit(args.tree.left, kLeft);
createContinuation(visit, {tree:args.tree.left,k:kLeft});
}
}
那么,原来的调用哪去了呢?现在这个visit似乎只是不断在改写全局变量continuation而已?别急,马上你就会明白怎么回事了:
function runIterate(){
while(continuation!=null){
var f = continuation.func;
var args = continuation.args;
continuation = null;
f(args); //思考:此处发生了什么?
}
}
runIterate函数可以轻易的在 continuation被改写的间隙执行这个 continuation所代表的函数动作!调用如下:
createContinuation(visit, {tree: mytree, k: print});
runIterate();
当然,可以改写为递归的执行:
function runRecursive(){
if(continuation){
var f = continuation.func;
var args = continuation.args;
continuation = null;
f(args);
runRecursive();
}
}
这样就完成了手写CPS的过程。这个过程中蕴含一些重要的思想。例如函数调用的地方不实际调用,而只是产生描述调用的数据,通过另一个执行函数去执行实际的函数调用。这个过程事实上是一个生成指令数据/解释指令执行的过程。这个思想在任何需要的地方都是一个不错的选择。