高阶流程控制语句(二): stackful coroutine
上次介绍了 stackless coroutine(generator) , 这次介绍真正具有完整功能的coroutine
首先看一下下面这段遍历二叉树的代码:
var travel = function(tree) { if (leaf(tree)) { yield tree; } else { /*???*/ travel(left(tree)); /*???*/ travel(right(tree)); } }; |
对于叶子节点, 直接yield就可以了, 但是对于递归的traval调用. ??? 处应该填什么?
方法1: 借助stackless coroutine实现stackful coroutine
回想一下上次实现的Fibonacci generator:
function fib() { var i = 0, j = 1; while (true) { yield i; var t = i; i = j; j += t; } } |
Fibonacci内部只需要保存两个状态, F(n-2)与F(n-1), 分别用i,j保存. 但是遍历二叉树, 我们需要保存的状态是 从当前叶子节点到根节点的整条路经, 遇到这种状态是动态的情况, stackless coroutine就无能为力了. 不过我们可以借助闭包, 把需要保存的状态记录在闭包里面, 这样使用stackless coroutine保存这单个闭包, 就可以了:
var join = function() { for (var index in arguments) { for (var i in arguments[index]) { yield i; } } }; var travel = function(tree) { if (leaf(tree)) { return (function() { yield tree; })(); } else { return join(travel(left(tree)), travel(right(tree))); } }; |
由于javascript调用函数是按值传递的, 上面这段代码运行到join函数之前, 会先计算join的参数, 递归调用travel函数, 导致很多次travel调用, 我们希望在开始遍历的时候再调用travel函数, 所以这里需要一次lazy优化:
var join = function() { for (var index in arguments) { for (var i in arguments[index]()) { yield i; } } }; var travel = function(tree) { if (leaf(tree)) { return (function() { yield tree; })(); } else { return join(function() { return travel(left(tree)); }, function() { return travel(right(tree)); }); } }; |
通过这个例子, 我们用stackless coroutine实现了stackful coroutine, 其中的关键, 就是这个join函数.
使用类似概念实现的coroutine有很多, 比如Kilim
[Kilim: Isolation-Typed Actors for Java. ( pdf ) ]
. 通过把需要Join的函数调用标记出来, 可以由编译器自动生成代码, 实现类似这里Join函数的工作. Kilim就是用@pausable这个annotation来标记需要Join的函数调用.
方法2: 使用CPS变换实现stackful coroutine
除了使用这个Join函数实现stackful coroutine之外, 还可以使用CPS变换实现stackful coroutine. 在流程控制领域, CPS变换几乎是万能的, 以后会解释为什么CPS是万能的.
首先, 从原始的traval函数开始:
var travel = function(tree) { if (leaf(tree)) { yield tree; } else { travel(left(tree)); travel(right(tree)); } }; |
然后, 对这个函数做CPS变换:
var $travel = function(k, tree) { if (leaf(tree)) { return $yield(k, tree); } else { return $travel(function() { return $travel(k, right(tree)); }, left(tree)); } }; |
最后, 把上面这个函数包装成generator:
var travel = function(tree) { var generator = {}; var $yield = function(k, result) { generator.next = k; return result; }; var $travel = function(k, tree) { if (leaf(tree)) { return $yield(k, tree); } else { return $travel(function() { return $travel(k, right(tree)); }, left(tree)); } }; generator.next = function() { return $travel(function() { throw "stopIterator"; }, tree); }; return generator; }; |
完整的代码如下, 各位可以试试, 看看里面是如何工作的:
var leaf = function(tree) { return typeof tree !== "object"; }; var left = function(tree) { return tree.l; }; var right = function(tree) { return tree.r; }; var travel = function(tree) { var generator = {}; var $yield = function(k, result) { generator.next = k; return result; }; var $travel = function(k, tree) { if (leaf(tree)) { return $yield(k, tree); } else { return $travel(function() { return $travel(k, right(tree)); }, left(tree)); } }; generator.next = function() { return $travel(function() { throw "stopIterator"; }, tree); }; return generator; }; var g = travel({ l: { l: 1, r: 2 }, r: 3 }); for (;;) { print(g.next()); } |
杂谈
javascript的语法真是又臭又长, 使用CoffeeScript重写了一下上面的代码, 发现简洁好多 (44 → 24 lines):
leaf = (tree) -> typeof tree != "object" left = (tree) -> tree.l right = (tree) -> tree.r travel = (tree) -> $yield = (k, result) -> generator.next = k result $travel = (k, tree) -> if leaf(tree) $yield(k, tree) else $travel((() -> $travel(k, right(tree))), left(tree)) generator = next: () -> $travel((() -> throw "stopIterator"), tree) g = travel l: l: 1 r: 2 r: 3 while true console.log(g.next()) |
虽然CoffeeScript仅仅是完全山寨javascript, 无任何创新, 但是最近却非常火热, 说明javascript实在是丑到家了