結局ジェネレータって何に使うの…?って話
JavaScript基礎力強化を図っています。
初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発
- 作者: Ethan Brown,武舎広幸,武舎るみ
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/01/20
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
この本を読み返しながら、知識のないところを学習するシリーズ。
※先に結論を話すと、表題に関して答えを見つけられませんでした😉
ジェネレータについて話す前に、イテレータについて。
JSのイテレータについてざっくり
他の言語にあるイテレータ同様、繰り返しのための機構で、JSの場合Array/String/Map/SetなどがIterable(イテレート可能)なオブジェクトです。
イテレート可能なオブジェクトは[Symbol.iterator]という名前で、iteratorプロトコルに準拠したメソッドを実装しています。(Symbol.iteratorについては前回の記事参照)
詳しくは↑の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();
こうか?
で、よりわけわからなくなった。
じゃあもうジェネレータって何に使うのよ、って感じですね。
はぁ…。