2022年8月30日

ジェネレーター

通常の関数は、1つの値のみ(または何も)を返します。

ジェネレーターは、必要に応じて複数の値を順番に返す(「yield」する)ことができます。それらはイテラブルと連携して動作し、データストリームを簡単に作成できます。

ジェネレーター関数

ジェネレーターを作成するには、特別な構文構造である「ジェネレーター関数」と呼ばれるfunction*が必要です。

それはこのように見えます

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

ジェネレーター関数は、通常の関数とは異なる動作をします。このような関数が呼び出されると、そのコードは実行されません。代わりに、「ジェネレーターオブジェクト」と呼ばれる特別なオブジェクトを返し、実行を管理します。

見てみましょう

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

// "generator function" creates "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]

関数コードの実行はまだ開始されていません

ジェネレーターの主なメソッドはnext()です。呼び出されると、最も近いyield <value>文(valueを省略するとundefined)まで実行されます。その後、関数の実行は一時停止し、yieldされたvalueが外部コードに返されます。

next()の結果は、常に2つのプロパティを持つオブジェクトです。

  • value:yieldされた値。
  • done:関数コードが終了した場合はtrue、そうでない場合はfalse

例えば、ここではジェネレーターを作成し、その最初のyieldされた値を取得します。

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

let one = generator.next();

alert(JSON.stringify(one)); // {value: 1, done: false}

現時点では、最初の値のみを取得しており、関数の実行は2行目です。

もう一度generator.next()を呼び出してみましょう。これによりコードの実行が再開され、次のyieldが返されます。

let two = generator.next();

alert(JSON.stringify(two)); // {value: 2, done: false}

そして、3回目に呼び出すと、実行は関数を終了するreturn文に到達します。

let three = generator.next();

alert(JSON.stringify(three)); // {value: 3, done: true}

これでジェネレーターは完了です。done:trueからそれがわかるはずです。そして、value:3を最終結果として処理します。

generator.next()への新しい呼び出しは、もはや意味がありません。呼び出した場合、同じオブジェクト{done: true}が返されます。

function* f(…)またはfunction *f(…)

どちらの構文も正しいです。

しかし、通常は最初の構文が優先されます。アスタリスク*は、それがジェネレーター関数であることを示しており、種類を表すものであり、名前を表すものではないため、functionキーワードに付くべきです。

ジェネレーターはイテラブルです

next()メソッドを見てお分かりのように、ジェネレーターはイテラブルです。

for..ofを使用して、その値をループ処理できます。

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, then 2
}

.next().valueを呼び出すよりもずっと見栄えが良いですよね?

…しかし、ご注意ください。上記の例では、1、次に2が表示され、それだけです。3は表示されません!

それは、for..of反復処理が、done: trueのときの最後のvalueを無視するためです。そのため、for..ofですべての結果を表示したい場合は、yieldで返す必要があります。

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, then 2, then 3
}

ジェネレーターはイテラブルであるため、スプレッド構文...など、関連するすべての機能を呼び出すことができます。

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

let sequence = [0, ...generateSequence()];

alert(sequence); // 0, 1, 2, 3

上記のコードでは、...generateSequence()は、イテラブルジェネレーターオブジェクトをアイテムの配列に変換します(スプレッド構文の詳細については、restパラメーターとスプレッド構文の章を参照してください)。

イテラブルのためのジェネレーターの使用

イテラブルの章では、以前、値from..toを返すイテラブルrangeオブジェクトを作成しました。

ここで、コードを思い出してみましょう。

let range = {
  from: 1,
  to: 5,

  // for..of range calls this method once in the very beginning
  [Symbol.iterator]() {
    // ...it returns the iterator object:
    // onward, for..of works only with that object, asking it for next values
    return {
      current: this.from,
      last: this.to,

      // next() is called on each iteration by the for..of loop
      next() {
        // it should return the value as an object {done:.., value :...}
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

// iteration over range returns numbers from range.from to range.to
alert([...range]); // 1,2,3,4,5

Symbol.iteratorとしてジェネレーター関数を提供することで、反復処理にジェネレーター関数を用いることができます。

これが同じrangeですが、はるかにコンパクトです。

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

alert( [...range] ); // 1,2,3,4,5

これは、range[Symbol.iterator]()がジェネレーターを返し、ジェネレーターメソッドがfor..ofが期待するものであるため機能します。

  • それは.next()メソッドを持っています。
  • それは{value: ..., done: true/false}という形式で値を返します。

もちろん、これは偶然ではありません。ジェネレーターは、イテレーターを実装しやすくするために、イテレーターを念頭に置いてJavaScript言語に追加されました。

ジェネレーターを使用した方法は、元のrangeのイテラブルコードよりもはるかに簡潔でありながら、同じ機能を維持しています。

ジェネレーターは永遠に値を生成できます

上記の例では有限のシーケンスを生成しましたが、無限に値をyieldするジェネレーターも作成できます。例えば、擬似乱数の無限シーケンスです。

そのようなジェネレーターに対するfor..ofでは、必ずbreak(またはreturn)が必要です。そうでなければ、ループは永遠に繰り返され、ハングします。

ジェネレーターの合成

ジェネレーターの合成は、ジェネレーターを互いに透過的に「埋め込む」ことができるジェネレーターの特別な機能です。

例えば、数値のシーケンスを生成する関数があるとします。

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

これを再利用して、より複雑なシーケンスを生成したいと考えています。

  • まず、数字0..9(文字コード48…57)、
  • 次に、大文字のアルファベットA..Z(文字コード65…90)、
  • 次に、小文字のアルファベットa..z(文字コード97…122)。

このシーケンスを使用して、例えば、そこから文字を選択してパスワードを作成できます(構文文字を追加することもできますが)、まずは生成してみましょう。

通常の関数では、複数の関数の結果を組み合わせるには、それらを呼び出し、結果を保存してから、最後に結合します。

ジェネレーターの場合、1つのジェネレーターを別のジェネレーターに「埋め込む」(合成する)ための特別なyield*構文があります。

合成されたジェネレーター

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {

  // 0..9
  yield* generateSequence(48, 57);

  // A..Z
  yield* generateSequence(65, 90);

  // a..z
  yield* generateSequence(97, 122);

}

let str = '';

for(let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

yield* genディレクティブは、ジェネレーターgenの実行を委任します。この用語は、yield* genがジェネレーターgenを繰り返し処理し、そのyieldを透過的に外部に転送することを意味します。まるで値が外部ジェネレーターによってyieldされたかのように。

結果は、ネストされたジェネレーターのコードをインライン化した場合と同じです。

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generateAlphaNum() {

  // yield* generateSequence(48, 57);
  for (let i = 48; i <= 57; i++) yield i;

  // yield* generateSequence(65, 90);
  for (let i = 65; i <= 90; i++) yield i;

  // yield* generateSequence(97, 122);
  for (let i = 97; i <= 122; i++) yield i;

}

let str = '';

for(let code of generateAlphaNum()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

ジェネレーターの合成は、1つのジェネレーターの流れを別のジェネレーターに挿入する自然な方法です。中間結果を保存するために余分なメモリを使用しません。

「yield」は双方向です

ここまでは、ジェネレーターは特別な構文で値を生成するイテラブルオブジェクトに似ていました。しかし、実際には、それらははるかに強力で柔軟です。

それは、yieldが双方向であるためです。それは結果を外部に返すだけでなく、ジェネレーター内部に値を渡すこともできます。

そのためには、引数付きでgenerator.next(arg)を呼び出す必要があります。その引数は、yieldの結果になります。

例を見てみましょう。

function* gen() {
  // Pass a question to the outer code and wait for an answer
  let result = yield "2 + 2 = ?"; // (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield returns the value

generator.next(4); // --> pass the result into the generator
  1. 最初の呼び出しgenerator.next()は、常に引数なしで行う必要があります(引数は渡されても無視されます)。これは実行を開始し、最初のyield "2+2=?"の結果を返します。この時点で、ジェネレーターは(*)行にとどまったまま、実行を一時停止します。
  2. 次に、上記の図のように、yieldの結果が呼び出し側のコードのquestion変数に入ります。
  3. generator.next(4)で、ジェネレーターが再開され、4が結果として取得されます:let result = 4

外部コードは、すぐにnext(4)を呼び出す必要はありません。時間がかかる可能性があります。それは問題ではありません。ジェネレーターは待ちます。

例えば

// resume the generator after some time
setTimeout(() => generator.next(4), 1000);

ご覧のとおり、通常の関数とは異なり、ジェネレーターと呼び出し側のコードは、next/yieldで値を渡すことで結果を交換できます。

さらに明確にするために、呼び出し回数の多い別の例を示します。

function* gen() {
  let ask1 = yield "2 + 2 = ?";

  alert(ask1); // 4

  let ask2 = yield "3 * 3 = ?"

  alert(ask2); // 9
}

let generator = gen();

alert( generator.next().value ); // "2 + 2 = ?"

alert( generator.next(4).value ); // "3 * 3 = ?"

alert( generator.next(9).done ); // true

実行図

  1. 最初の.next()は実行を開始します…最初のyieldに到達します。
  2. 結果は外部コードに返されます。
  3. 2番目の.next(4)は、最初のyieldの結果として4をジェネレーターに渡し、実行を再開します。
  4. …2番目のyieldに到達し、それがジェネレーター呼び出しの結果になります。
  5. 3番目のnext(9)は、2番目のyieldの結果として9をジェネレーターに渡し、実行を再開します。関数の最後に到達するため、done: trueとなります。

これは「ピンポン」ゲームのようなものです。各next(value)(最初のものを除く)はジェネレーターに値を渡し、それは現在のyieldの結果になり、次に次のyieldの結果を取得します。

generator.throw

上記の例で見たように、外部コードは、yieldの結果としてジェネレーターに値を渡すことができます。

…しかし、そこでエラーを開始(スロー)することもできます。エラーは一種の結果であるため、自然です。

yieldにエラーを渡すには、generator.throw(err)を呼び出す必要があります。その場合、errはそのyieldのある行でスローされます。

例えば、ここでは"2 + 2 = ?"のyieldがエラーにつながります。

function* gen() {
  try {
    let result = yield "2 + 2 = ?"; // (1)

    alert("The execution does not reach here, because the exception is thrown above");
  } catch(e) {
    alert(e); // shows the error
  }
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("The answer is not found in my database")); // (2)

(2)行でジェネレーターにスローされたエラーは、yieldのある(1)行で例外になります。上記の例では、try..catchがそれをキャッチして表示します。

それをキャッチしない場合、他の例外と同様に、ジェネレーターから呼び出し側のコードに「流れ落ち」ます。

呼び出し側のコードの現在の行は、generator.throwのある行であり、(2)とラベル付けされています。したがって、次のようにここでキャッチできます。

function* generate() {
  let result = yield "2 + 2 = ?"; // Error in this line
}

let generator = generate();

let question = generator.next().value;

try {
  generator.throw(new Error("The answer is not found in my database"));
} catch(e) {
  alert(e); // shows the error
}

そこでエラーをキャッチしない場合、通常どおり、外部の呼び出し側コード(もしあれば)に流れ落ち、キャッチされないとスクリプトが停止します。

generator.return

generator.return(value)はジェネレーターの実行を終了し、指定されたvalueを返します。

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const g = gen();

g.next();        // { value: 1, done: false }
g.return('foo'); // { value: "foo", done: true }
g.next();        // { value: undefined, done: true }

完了したジェネレーターで再度generator.return()を使用すると、その値が再び返されます(MDN)。

多くの場合、ほとんどの場合、すべての戻り値を取得したいので使用しませんが、特定の条件でジェネレーターを停止したい場合に役立ちます。

まとめ

  • ジェネレーターは、ジェネレーター関数function* f(…) {…}によって作成されます。
  • ジェネレーター内(のみ)には、yield演算子が存在します。
  • 外部コードとジェネレーターは、next/yield呼び出しを介して結果を交換できます。

最新のJavaScriptでは、ジェネレーターはめったに使用されません。しかし、実行中に関数と呼び出し側のコードがデータ交換できる機能は非常にユニークであるため、場合によっては役立ちます。そして、確かに、イテラブルオブジェクトを作成するのに最適です。

また、次の章では、非同期ジェネレーターについて学びます。これは、for await ... ofループで非同期的に生成されたデータのストリーム(例:ネットワーク上のページネーションされたフェッチ)を読み取るために使用されます。

ウェブプログラミングでは、ストリーミングデータを取り扱うことがよくあります。そのため、これは非常に重要なユースケースでもあります。

課題

ランダムなデータが必要な場面はたくさんあります。

その一つがテストです。テストを適切に行うために、ランダムなテキスト、数値などのデータが必要になる場合があります。

JavaScriptでは、`Math.random()`を使用できます。しかし、何か問題が発生した場合、まったく同じデータを使用してテストを繰り返せるようにしたいです。

そのため、「シード付き疑似乱数生成器」と呼ばれるものが使用されます。これらは「シード」(最初の値)を取り、数式を使用して次の値を生成します。そのため、同じシードは同じシーケンスを生成し、全体の流れを簡単に再現できます。繰り返すには、シードを覚えておくだけで済みます。

比較的均一に分布した値を生成する数式の例

next = previous * 16807 % 2147483647

シードとして`1`を使用した場合、値は次のようになります。

  1. 16807
  2. 282475249
  3. 1622650073
  4. …以下同様…

課題は、`seed`を受け取り、この数式を使用してジェネレータを作成するジェネレータ関数`pseudoRandom(seed)`を作成することです。

使用例

let generator = pseudoRandom(1);

alert(generator.next().value); // 16807
alert(generator.next().value); // 282475249
alert(generator.next().value); // 1622650073

テストを含むサンドボックスを開いてください。

function* pseudoRandom(seed) {
  let value = seed;

  while(true) {
    value = value * 16807 % 2147483647;
    yield value;
  }

};

let generator = pseudoRandom(1);

alert(generator.next().value); // 16807
alert(generator.next().value); // 282475249
alert(generator.next().value); // 1622650073

同じことは、次のような通常の関数でも行うことができます。

function pseudoRandom(seed) {
  let value = seed;

  return function() {
    value = value * 16807 % 2147483647;
    return value;
  }
}

let generator = pseudoRandom(1);

alert(generator()); // 16807
alert(generator()); // 282475249
alert(generator()); // 1622650073

これも機能します。しかし、`for..of`による反復処理や、他の場所で役立つ可能性のあるジェネレータ合成の機能が失われます。

テストを含む解答をサンドボックスで開いてください。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点に関する提案がある場合は、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数行のコードを挿入するには`<code>`タグを使用し、複数行の場合は`<pre>`タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。