JS.next

JavaScriptの最新実装情報を追うブログ

ジェネレータについて

概要

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(デフォルト有効)