ジェネレータについて
概要
V8でジェネレータ周りの実装が進んできたので、解説してみようと思う。
ジェネレータ関数
ジェネレータ関数とは、(一つの見かたとしては)処理を途中で一時停止できる関数のことである。
例えば、呼び出される度に数を順番に返す関数を定義したいとする。
function count(n) { return function () { return n++ } } var next = count(10) next() // 10 next() // 11 next() // 12
これがジェネレータ関数を用いると次のように書ける。
function* count(n) { while (true) { yield n++ } } var gen = count(10) gen.next() // { value: 10, done: false } gen.next() // { value: 11, done: false } gen.next() // { value: 12, done: false }
「function」に続いて「*」を使うことによりジェネレータ関数を宣言できる。
ジェネレータ関数を呼び出すと、ジェネレータ(イテレータオブジェクト)が返される。
このジェネレータをnextメソッドで進めると、次のyield式まで処理を進め、式の結果の値が返される。
「yield」はジェネレータ関数内で有効なキーワードで、式を作る。
returnの親戚のようなもので、関数はそこで外部に値を返して、終了する代わりに一時停止する。
再開するときに値を受け取る
ジェネレータのnextメソッドに値を与えると、ジェネレータ関数を再開するときに値を渡す事ができる。
渡した値は、yield式の結果の値となる。
function* gfn() { var v = yield 123 console.log(v) } var gen = gfn() gen.next() // { value: 123, done: false } gen.next(456) // { value: undefined, done: true } /* log 456 */
function* gfn() { return yield yield yield } var gen = gfn() gen.next() // { value: undefined, done: false } gen.next(1) // { value: 1, done: false } gen.next(2) // { value: 2, done: false } gen.next(3) // { value: 3, done: true } gen.next(4) // { value: undefined, done: true } gen.next(5) // { value: undefined, done: true }
function* gfn() { return [yield, yield, yield] } var gen = gfn() gen.next() // { value: undefined, done: false } gen.next(1) // { value: undefined, done: false } gen.next(2) // { value: undefined, done: false } gen.next(3) // { value: [1, 2, 3], done: true }
ジェネレータ関数で例外が発生したら
ジェネレータのnextメソッド呼び出し元まで伝わる。
function* gfn() { throw 'err' } var gen = gfn() gen.next() // Error "err"
エラーが起きた後さらにジェネレータを操作しようとするとエラーになる。
function* gfn() { throw 'err' } var gen = gfn() try { gen.next() } catch(_) {} gen.next() // Error
ジェネレータ関数をエラーと共に再開させる
ジェネレータのthrowメソッドを呼ぶと、ジェネレータ関数を再開させるときに例外を起こすとができる。
function* gfn() { try { yield 1 } catch(err) { console.log('error!') } yield 2 } var gen = gfn() gen.next() // { value: 1, done: false } gen.throw(Error('err')) // { value: 2, done: false } /* log "error!" */
ジェネレータ関数を終了させる (未実装)
ジェネレータのreturnメソッドを呼ぶと、ジェネレータ関数が再開され即returnされたように振る舞う。
function* gfn(n) { yield 1 yield 2 yield 3 } var gen = gfn() gen.next() // { value: 1, done: false } gen.return(42) // { value: 42, done: true } gen.next() // { value: undefined, done: true }
function* count(n) { while (true) yield n++ } var gen = count(1) for (var v of gen) { if (v%10 == 0) gen.return() } v // 10
yield*キーワード
yield*キーワードを使うと、値をそのまま返す代わりにイテラブルとして扱い、イテレートされたそれぞれの値に対してyield演算子を使ったのと同じ処理をする。
つまりこれが
function* gfn() { var iterable = [1, 2, 3] for (var v of iterable) yield v }
こう書ける
function* gfn() { var iterable = [1, 2, 3] yield* iterable }
ジェネレータもイテラブルなので、
function* count(v) { yield v yield* count(v + 1) } var gen = count(10) gen.next() // { value: 10, done: false } gen.next() // { value: 11, done: false } gen.next() // { value: 12, done: false }
非同期処理を同期的に扱う
ここまで見てきたように、ジェネレータ関数は値を一旦返し、任意のタイミングで任意の値と共に再開させることができる。
つまりPromiseを返し、外部でその解決を待ってから解決値を与え再開させることで、非同期処理を同期的に記述することができる。
例えばこういったPromiseを返す関数がいくつか用意されている時、
function fetch(url, type) { return new Promise(function (ok, ng) { var xhr = new XMLHttpRequest xhr.open('GET', url) if (type) xhr.responseType = type xhr.onloadend = function () { if (xhr.status == 200) ok(xhr.response) else ng(new Error('404')) } xhr.send() }) }
とあるゲームプレイヤーのソースはこうなる。
function main() { return fetch('setting.json', 'json').then(function (setting) {// 設定ファイルを取り込む var games = setting.games // 選べるゲームの名前を取得 return showChoiceWindow(games).then(function (game) { // 選んで貰う var dir = setting.GameDirectory + game + '/' return fetch(dir + 'config.json', 'json').then(function (config) { return runGame(game, config) // ゲームを開始する }).then(main) // 終了したら初めから繰り返す }) }) } main().catch(ERROR)
それまでの結果を利用しようとすると、なかなかネストが避けられない。
ネストを避けても、沢山の関数定義が並ぶとあまり読みやすくない。
そこで、ジェネレータ関数を受け取り、ジェネレータが返すPromiseの解決を待って、ジェネレータを進める魔法の関数「spawn」を使うとこうなる。
var main = spawn(function* () { var setting = yield fetch('setting.json', 'json') // 設定ファイルを取り込む var games = setting.games // 選べるゲームの名前を取得 var game = yield showChoiceWindow(games) // 選んで貰う var dir = setting.GameDirectory + game + '/' var config = yield fetch(dir + 'config.json', 'json') yield runGame(game, config) // ゲームを開始する main() // 終了したら初めから繰り返す }) main().catch(ERROR)
魔法の関数の作り方
「spawn」はジェネレータ関数を受け取って関数を返す。
function spawn(gfn) { return function () { } }
返された関数を呼ぶと、ジェネレータ関数を呼んで、例の処理をする。
function spawn(gfn) { return function () { var gen = gfn.apply(this, arguments) function step(v) { var r = gen.next(v) if (!r.done) r.value.then(step) } step() } }
更に返される関数が処理結果のPromiseを返すようにしたりするとこんな感じになる。
function spawn(gfn) { return function () { var gen = gfn.apply(this, arguments) return new Promise(function (resolve, reject) { function step(v) { var r = gen.next(v) Promise.resolve(r.value).then(r.done ? resolve : step).catch(reject) } step() }) } }
実装されるバージョン
V8 -3.28.59(return以外) 3.29.70(デフォルト有効)
Chrome M39(デフォルト有効)