Help us understand the problem. What is going on with this article?

ES2015新機能: ジェネレータをマスターしたい

More than 3 years have passed since last update.

Koaをやるにあたって、やっぱりジェネレータを知らなければ話にならないようです。
「ジェネレータを使ってコールバックを使わずに非同期処理できる!」と聞いても、「なんでやねん。。。」としかならなかったので、ここは一つ、腰を据えてジェネレータについて勉強しておきたいところです

ジェネレータとは

ジェネレータの意味

ジェネレータは「生成器」なんて訳されていますが、プログラムの世界では次々と値を生成する仕組みのことのようです。

もう少し狭義の世界で考えると、イテレータを実現する仕組みの一種となっていたりもします。
PHPにも5.5以降はジェネレータの仕組みが有りますが、そこでは「イテレータを簡単に実装できる仕組み」となっています。
Javascript の解説ページなどを見ると、やはりイテレータとセットになっている所も多いようです

ですが、どちらかと言うとジェネレータはあくまで値を次々生成する仕組みであり、機能の一部としてイテレーションが入っているように思います

ジェネレータの実装

ジェネレータを早速実装してみましょう

'use strict'

// ジェネレータの定義
function* MakeNumber() {
  yield 1;
  yield 2;
  yield 3;
}

let gen = MakeNumber();// ジェネレータの初期化

console.log(gen.next());// { value: 1, done: false }
console.log(gen.next());// { value: 2, done: false }
console.log(gen.next());// { value: 3, done: false }
console.log(gen.next());// { value: undefined, done: true }

ジェネレータの定義方法は、functionの代わりにfunction*で定義し、定義の中で幾つかのyieldが含まれています。
次にジェネレータを初期化し変数genをジェネレータにします。
ジェネレータgennextメソッドを持っています。
nextの役割は以下のとおりです

  1. ジェネレータ関数の定義の現在位置からyieldのある位置まで処理をすすめる
  2. yieldキーワードの後に書かれた式を返却値とし、処理位置をそこで止める
  3. 正確な返却値は{value: <yieldキーワードの後に書かれた式>, done: false}となる
  4. 後ろにyieldがない状態でnextを呼ぶと、{value: undefined, done: true}を返す

つまり、nextメソッドを呼ぶたびに、各yieldまで処理が進み、一旦値を返すという処理が繰り返されます。
最後のyieldの処理が終わると、もう値を返せないので以降のnextvalueがなしで(value: undefined)、ジェネレータの処理が完了しているサイン(done: true)を返します。

無限生成

ループとyieldを組み合わせることで、無限に生成し続けるジェネレータを作ることができます

'use strict'

// 階乗計算
function* Factorial() {
  let i = 1;
  let num = 1;
  while (true) {
    num *= i++
    yield (i - 1) + '! = ' + num;
  }
}

let gen = Factorial();// ジェネレータの初期化

console.log(gen.next());// { value: '1! = 1', done: false }
console.log(gen.next());// { value: '2! = 2', done: false }
console.log(gen.next());// { value: '3! = 6', done: false }
console.log(gen.next());// { value: '4! = 24', done: false }

これはいくらnextを呼びつづけても、延々と値を生成し続けますので、donetrueになることはないでしょう

イテレータとジェネレータ

イテレータの反復処理の対象としてジェネレータを選択することができます。

'use strict'

// 階乗計算
function* Factorial() {
  let i = 1;
  let num = 1;
  while (i < 7) {
    num *= i++
    yield {number: (i - 1), fact: num}
  }
}

let gen = Factorial();// ジェネレータの初期化

// 反復処理の対象としてジェネレータを選択する
for (let obj of gen) {
  console.log(obj.number + '! = ' + obj.fact);
  if (obj.number >= 10) {
    break;
  }
}

結果はこんな感じです

1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720

まず、反復対象としてジェネレータを選んだ場合、各要素objにはは'next'メソッドで返却されるオブジェクトのvalueが代入されることがわかります。
続いてfor文の中ではobj.numberが10になるまではループを続けられるようになっていますが、実際にはobj.numberは6までで打ち止めになっています。
これはnextメソッドで返却されるオブジェクトのdonetrueになっていると、反復処理が終了することを意味しています。

ジェネレータの中のジェネレータ

yield* を使うことで、ジェネレータの中で別のジェネレータに処理を移譲することもできます。

'use strict'

// 入れ子のジェネレータ
function* Subgen() {
  yield 3;
  yield 4;
}

function* Maingen() {
  yield 1;
  yield 2;
  yield* Subgen();
  yield 5;
}

let gen = Maingen();// ジェネレータの初期化

for (let num of gen) {
  console.log(num);
}

結果は以下のとおりです

1
2
3
4
5

このyield*は、ジェネレータにかぎらず反復処理の対象にできるものであれば何でも指定できるので、例えばyield* Subgen()の部分をyield* [3, 4]に書き換えても、全く同じ動作をします。

yieldの「返り値」

yieldはこれまで見てきたように、ジェネレータの中ではその処理を中断し、右にある式の値を返します。
では、再度nextを呼び出して処理を行うとき、どこから始まるのでしょうか?
答えは簡単で、yieldから始まります。
しかも、このとき、yieldにはnextメソッドに与えられた引数が展開されます。
次のコードを見てください

'use strict'

function* Maingen() {
  while (true) {
    let x = yield 1;
    console.log(x);
  }
}

let gen = Maingen();

gen.next();// <何も出力されない>
gen.next();// undefined
gen.next(1);// 1
gen.next();// undefined
gen.next('A');// A

一回目のnextでは、そもそもconsole.logを通過する前に止まってしまうので、何も出力されません。
続いて、二回目のnextでは引数に何も指定していないため、ジェネレータ内部のxには何も代入されません。したがってundefinedが出力されます。
三回目のnextには1が引数として与えられています。すると、yieldに1が入り、xに1が代入されます。したがって、console.logによって1が出力されるというわけです。
その後も内容は同じです。四回目は何も指定していないので、undefinedになり、五回目は'A'を指定したので、Aが出力されます。

まとめ

ES2015の目玉機能であるところのジェネレータについて見てみました。
このジェネレータが非同期処理とどう絡むかについて、次回見てみたいと思います。

参考

イテレータとジェネレータ - JavaScript | MDN
PHP: ジェネレータとは - Manual

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away