Javascript New Keyword: Yield

生成器与迭代器

在以前写代码的时候, 涉及到迭代算法时, 通常整个过程中都需要维护一个状态变量, 而我们想使用迭代算法的中间值得时候, 不得不使用回调函数.

下面是一个斐波那契数列的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function do_callback(num) {
  console.log(num);
}
function fib() {
  var i = 0, j = 1, n = 0;
  while (n < 10) {
    do_callback(i);
    var t = i;
    i = j;
    j += t;
    n++;
  }
}
fib();

上面的代码中使用了回调函数, 将小于 10 的斐波那契数列的元素输出到控制台.

迭代器和生成器提供了一个新的, 更好的途径来做同样的事情. 下面是使用生成器实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function *fib() {
  var i = 0, j = 1;
  while (true) {
    yield i;
    var t = i;
    i = j;
    j += t;
  }
}
var g = fib();
for (var i = 0; i < 10; i++) {
  console.log(g.next().value);
}

上面的 function *fib, 函数体中含有关键字 yield 的函数就是生成器. 当你调用它的时候, 它的形参绑定到实参, 但函数本身并不进行求值, 而是返回一个 generator-iterator, 每调用一次 generator-iteratornext 方法, 迭代算法会进行一次. 每一步产生的值都由关键字 yield 返回. yield 关键字可以看做 generator-iterator 的返回值, 它代表迭代算法每次迭代的分界线. 每当调用 next 方法时, 生成器会接着从 yield 语句紧接着的下一句恢复状态继续执行.

我们循环调用生成器的 next 方法, 直到我们想要的结果, 上面的例子中, 我们打印了斐波那契数列的前 10 项. 但是生成器的版本允许我们生成任意多项, 只要继续循环调用 next 方法即可.

对于那些认为 yield, 生成器仅仅是语法糖的人, 我只能表示: 呵呵. 如果一个能影响编程时的思考方式的东西也叫做语法糖的话, 那么 C 是汇编的语法糖, C++ 就是 C 的语法糖了.

yield 关键字有了概念之后, 我们来看看它都给我们带来了些什么.

生成器是产生迭代器更好的方式

学过 python 的同学应该知道 rangexrange 的区别. 如果不清楚的话, 见这个问题. 没错, range 会返回一个 list, 而 xrange 却是惰性求值的. (不是严格意义上的生成器, 但特性类似)

因此, 当我们需要遍历一个非常非常大的列表时, 一次性返回全部结果, 非常的消耗内存, 显然不是一个可行的办法. 我们需要惰性求值, 需要迭代器. 但实现一个迭代器, 我们需要手工维护迭代器的内部状态, 实现 next 方法等等. 此时就该轮到我们的生成器登场了.

使用生成器我们可以仅仅使用一个 function 就实现一个具有内部状态的迭代算法. That’s awesome!

我在写 roar 时, 实现惰性求值是一个纠结的问题, 由于 C++ 没有生成器类似的特性, 迭代器都是手工完成的, 需要手工维护迭代器状态, 容易出错, 而且实现其他功能也会带来阻碍, 举个例子, 如果 C++ 支持 yield 的话, 那么 LINQ 的 concat 函数一个可能的实现就是下面这样了:

1
2
3
4
5
6
7
8
9
template <typename Collection>
typename Collection::iterator concat(Collection a, Collection b) {
  for(auto x : a) {
      yield x;
  }
  for(auto x : b) {
      yield x;
  }
}

从思考到实现都是那么的流畅, 而不用像现在一样还在纠结如何才能在不对 Collection 进行合并操作就能让迭代器很好的工作这个问题了.

yield 不仅仅为迭代器而生

本质上, yield 语句就是利用编译器/解释器替我们做了CPS变换而已. 而CPS变化是控制流的最主要武器. 也许, yield 能帮我们简化控制流, 远离 callback hell?

先来回顾一下 yield 的用法及执行过程, 当遇到 yield 语句时, 生成器函数暂停, 交出控制权, 当外部指定生成器函数继续时(调用 next 方法), 控制权又交回生成器函数继续执行直到下一个 yield 语句. 如果我们能够将所有耗时/已经是异步 callback 的函数更改为 yield, 然后在 callback 中直接调用生成器的 next 方法, 那么我们就可以完全的抛弃 callback 来优雅的完成控制流. 已经有人利用生成器的特性完成这个功能了. 见 suspend.

对异步编程稍微有点研究的话, 相信不难从上面看到 await 的影子, async 和 await 是 C# 5.0 中引入的关键字, 它使得异步编程变得更简单, await 与 yield 类似, 会暂停并交出控制权, 之前的 异步编程和延续传递风格异步编程 async & await 提到过相关内容. C++ 也会引入相应的标准, Resumable Functions 的提案就是 async & await 的 C++ 版.

遗憾的是, 虽然 yield 虽然进入了 ES6, 但是 await 却没有. 如果 await 能够进入标准的话, 那么也许可以早日改善流程控制库满天飞的局面. ╮(╯▽╰)╭

参考资料

Comments