ゆるふわ技術日誌

エンジニア見習いの悪戦苦闘日記

結局ジェネレータって何に使うの…?って話

JavaScript基礎力強化を図っています。

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

この本を読み返しながら、知識のないところを学習するシリーズ。


※先に結論を話すと、表題に関して答えを見つけられませんでした😉

ジェネレータについて話す前に、イテレータについて。

JSのイテレータについてざっくり

他の言語にあるイテレータ同様、繰り返しのための機構で、JSの場合Array/String/Map/SetなどがIterable(イテレート可能)なオブジェクトです。

イテレート可能なオブジェクトは[Symbol.iterator]という名前で、iteratorプロトコルに準拠したメソッドを実装しています。(Symbol.iteratorについては前回の記事参照)

developer.mozilla.org

詳しくは↑のMDNをみてもらうとして、ざっくり説明すると、

  • nextという名前のメソッドを持つ
  • 呼び出した時に{value: any, done: boolean}なオブジェクトを返す

というのがイテレータの条件です。

上記の条件を満たすことでfor..ofが使えたりします。もちろん自作のクラスに対して実装してあげてもOK。

で、ジェネレータは?

で、なぜイテレータについての説明を先にしたかというと、ジェネレータは呼び出すとイテレータを返す特殊な関数のことです。

こんな感じで定義します。

function* generator() {
  yield 'hoge'
  yield 'fuga'
  yield 'piyo'
  return
}

function*yieldという特別なキーワードを使って宣言するのが特徴で、この関数を呼び出すとイテレータがかえります。

const it = generator();
it.next(); // -> {value: 'hoge', done: false}
it.next(); // -> {value: 'fuga', done: false}
it.next(); // -> {value: 'piyo', done: false}
it.next(); // -> {value: undefined, done: true}

こんな感じ。

何が起こっているかというと、ジェネレータが返したイテレータを実行していくと、ジェネレータ内のyieldまで実行して、渡された値を返します。 実行はその時点で一旦ストップし、次に呼ばれたら、前回のyieldの位置の続きから実行して、次にyieldが出てきたら、渡された値を返します。

最後、returnが呼ばれるとイテレータはdone: trueを返すようになります。

また、イテレータのnextメソッドには引数を渡すことができ、

function* generator() {
  const name = yield 'What is your name?';
  console.log(`Your name is ${name}.`);
  return;
}

const it = generator();

it.next(); // -> {value: 'What is your name?', done: false}
it.next('mogami');
// Your name is mogami.
// -> {value: undefined, done: true}

みたいな感じで、前回実行したyieldの返り値がnextに渡した値になります。

何に使えるのか

やっとここで何に使えるのか、という話になります。

オライリー本では、イテレータ/ジェネレータの説明は12章で行われているのですが、14章 非同期プログラミングのところで再度ジェネレータが出てきます。

ジェネレータランナーという関数を用意することで、非同期処理が同期的に書けるという話。

本のソースコードを引用すると、

function grun(g) {
  const it = g();

  (function iterate(val) {
    const x = it.next();
    if (!x.done) {
      if (x.value instanceof Promise) {
        x.value.then(iterate).catch(err => it.throw(err));
      } else {
        setTimeout(iterate, 0, x.value);
      }
    }
  })();
}

こんな感じの関数を用意してやって

function* fileReadAndWrite() {
  const dataA = yield readFile('a.txt');
  const dataB = yield readFile('b.txt');
  const dataC = yield readFile('c.txt');  
  yield writeFile('d.txt', dataA+dataB+dataC);
}

(readFile/writeFileはいずれもPromiseを返す関数)

最後にこのジェネレータを最初に作ったジェネレータランナーに投げ込む

grun(fileReadAndWrite);

すると、非同期処理が同期的に書けて人間にみてもわかりやすいよね、と書いてありました。

とはいえ、コレって今となってはasync/awaitで解決しちゃう問題なんですよね…。

async function fileReadAndWrite() {
  const dataA = await readFile('a.txt');
  const dataB = await readFile('b.txt');
  const dataC = await readFile('c.txt');  
  await writeFile('d.txt', dataA+dataB+dataC);
}

fileReadAndWrite();

こうか?

で、よりわけわからなくなった。

じゃあもうジェネレータって何に使うのよ、って感じですね。

はぁ…。