Category tech.

ES2015 の iterable/iterator/generator による無限 FizzBuzz | tech - 氾濫原 に続いて、オブジェクト指向っぽく書けるようにしてみました。

ポイントはジェネレータ的なものをラップして常に It というクラスのインスタンスにするところです。

"use strict";

function It(iterable) { if (typeof this === 'undefined') return new It(iterable); this.iterable = iterable; }
It.countup = function* (n) {
	for (;;) yield n++;
};
It.prototype = {
	zip : function* () {
		const iterators = [];
		for (let it of this) {
			iterators.push(it[Symbol.iterator]());
		}
		for (;;) {
			const nexts = [];
			for (let it of iterators) {
				nexts.push(it.next());
			}
			yield nexts.map( (i) => i.value);
			if (nexts.some( (i) => i.done) ) break;
		}
	},
	map : function* (func) {
		for (let value of this) {
			yield func(value);
		}
	},
	cycle : function* () {
		for (;;) {
			for (let value of this) {
				yield value;
			}
		}
	},
	take : function (n) {
		const ret = [];
		for (let value of this) {
			ret.push(value);
			if (!(ret.length < n)) break;
		}
		return ret;
	}
};
It.prototype[Symbol.iterator] = function () { return this.iterable[Symbol.iterator]() };
{
	let wrapGenerator = function (generator) {
		return function () {
			const self = this;
			const args = arguments;
			const iterable = {};
			iterable[Symbol.iterator] = function () {
				return generator.apply(self, args);
			}
			return new It(iterable);
		}
	};
	let generatorConstructor = Object.getPrototypeOf(function*(){}).constructor;
	for (let key of Object.keys(It.prototype)) {
		if (It.prototype[key] instanceof generatorConstructor) {
			It.prototype[key] = wrapGenerator(It.prototype[key]);
		}
	}
}


console.log(Array.from(
	It([
		It.countup(1),
		It(["", "", "Fizz"]).cycle(),
		It(["", "", "", "", "Buzz"]).cycle()
	]).
		zip().
		map( (_) => _[1] + _[2] || _[0] ).
		take(30)
));
  1. トップ
  2. tech
  3. ES2015 の iterable/iterator/generator による無限 FizzBuzz (オブジェクト指向編)

表題の通りですが、Generator にはいずれの protocol も実装されています。気になるのは iterable の挙動ですが、どうやらレシーバーの Generator 自身を返すようです。

function* count (n) {
	for (;;) yield n++;
}

var c = count(1);
console.log(c.next, c[Symbol.iterator]);
//=> [Function: next] [Function: [Symbol.iterator]]

// iterator protocol
console.log(c.next()); //=> { value: 1, done: false }
console.log(c.next()); //=> { value: 2, done: false }
console.log(c.next()); //=> { value: 3, done: false }

// iterable protocol
console.log(c[Symbol.iterator]().next()); //=> { value: 4, done: false }

console.log(c[Symbol.iterator]() === c); //=> true

iterator protocol (next) で状態をすすめたあとに iterable protocol (Symbol.iterator) で iterator を取得すると、状態は継続されています。

  1. トップ
  2. tech
  3. Generator は iterator であり、iterable でもある

ES2015 の iterable protocol / iterator protocol だとそこそこ自然に無限リストを作れるわけなので、ちょっと試しにやってみました。node v5.2.0 で動かしました。

"use strict";


function* countup(n) {
	for (;;) yield n++;
}


function* map(iterable, func) {
	for (let value of iterable) {
		yield func(value);
	}
}


function* cycle(iterable) {
	for (;;) {
		for (let value of iterable) {
			yield value;
		}
	}
}


function take(iterable, n) {
	const ret = [];
	for (let value of iterable) {
		ret.push(value);
		if (!(ret.length < n)) break;
	}
	return ret;
}


function* zip(iterable) {
	for (;;) {
		const nexts = [];
		for (let it of iterable) {
			nexts.push(it.next());
		}
		yield nexts.map( (i) => i.value);
		if (nexts.some( (i) => i.done) ) break;
	}
}




console.log(
	take(
		map(
			zip([
				countup(1),
				cycle(["", "", "Fizz"]),
				cycle(["", "", "", "", "Buzz"])
			]),
			(_) => _[1] + _[2] || _[0]
		),
		30
	)
);

FizzBuzz の map のところは destructive assignment ができるともうちょい綺麗に書けますが、現時点だとまだオプションが必要なのでキモい書きかたになりました。

気になった点

protocol と言っている通り、next() を適切なインターフェイスで実装しているものは全て iterator なため、イテレータ全般に対して基底クラスみたいなものがありません。これはこれでいいんですが、不便な点があります。イテレータに対してメソッドの追加というのができません。

自分の中のオブジェクト指向の気持ちは以下のように書きたいのです。

[
	countup(1),
	cycle(["", "", "Fizz"]),
	cycle(["", "", "", "", "Buzz"])
].
	zip().
	map( (_) => _[1] + _[2] || _[0] ).
	take(30)

しかし、イテレータの prototype というのは存在しないので、毎回必ず何らかの関数形式のラッパーが必要になってしまいます。

  1. トップ
  2. tech
  3. ES2015 の iterable/iterator/generator による無限 FizzBuzz