React入門者へおすすめ動画&書籍おすすめ!

フロントエンド入門者へおすすめ動画&書籍おすすめ!

JavaScript

JSのGeneratorsについて分かりやすく使い方や基礎を学んでみる

0
0

ジェネレータ

 

通常の関数は、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 は省略可能で、その場合は未定義となります)。

 

その後、関数の実行は一時停止し、yieldされた値が外部コードに返されます。

 

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

 

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

 

例えば、ここではジェネレータを作成し、その最初のyielded値を取得しています。

 

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

let generator = generateSequence();

let one = generator.next();

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

 

今のところ、1つ目の値だけを取得し、関数の実行は2行目にあります。

 

 

generator.next()をもう一度呼んでみましょう。

 

コードの実行が再開され、次のyieldが返されます。

 

 

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

 

 

let three = generator.next();

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

 

 

 

これでジェネレーターが完成しました。

 

最終結果としてdone:true、process 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のときに最後の値を無視するからです。

 

ですから、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
}

 

ジェネレータは反復可能なので、

 

関連するすべての機能を呼び出すことができます。

 

例えば、spread syntax ...: (スプレッド構文)

 

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

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

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

 

上のコードでは、...generateSequence()が反復可能なジェネレータオブジェクトをアイテムの配列に変えています

 

(スプレッド構文については、過去の記事をお読みください)

 

 

 

反復子のためのジェネレータの使用

 

from...toの値を返す反復可能な範囲オブジェクトを作成しましたので。

 

ここで、そのコードを少し見てみます。

 

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()は、for...ofループの繰り返しごとに呼び出されます。
      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 };
        }
      }
    };
  }
};

// 範囲の反復処理では、range.fromからrange.toまでの数値を返します。
alert([...range]); // 1,2,3,4,5

 

ジェネレータ関数を Symbol.iterator として提供することで、イテレーションに使用することができます。

 

これは同じ範囲ですが、よりコンパクトになっています。

 

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

 

rangeSymbol.iteratorがジェネレータを返すようになり、ジェネレータメソッドはまさにfor...ofが期待するものであるため、うまくいきました。

 

  • .next()メソッドがあり、{value: ..., done: true/false}の形で値を返します。

もちろん、これは偶然の産物ではありません。

 

ジェネレータは、イテレータを簡単に実装するために、イテレータを念頭に置いてJavaScript言語に追加されました。

 

ジェネレータを使用したバリアントは、元々のイテレータのコードであるrangeよりもはるかに簡潔で、同じ機能を維持しています。

 

メモ

ジェネレータは永遠に値を生成することができる

上の例では、有限の配列を生成しましたが、永遠に値を生成するジェネレータを作ることもできます。例えば、終わりのない疑似乱数の列です。

しかし、そのようなジェネレータのfor...の中で、ブレーク(またはリターン)が必要になるでしょう。そうしないと、ループが永遠に繰り返され、ハングアップしてしまいます。

 

ジェネレータ・コンポジション

 

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

 

例えば、数字の列を生成する関数があるとします。

 

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)

 

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

 

ジェネレータの場合、あるジェネレータを別のジェネレータに「埋め込む」(合成する)ための特別な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* 指令は,実行を別のジェネレータに委ねるものです.

 

この言葉は,yield* genがジェネレータgenを反復し,その収量を透過的に外部に転送することを意味します.

 

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

 

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

 

ジェネレータ・コンポジションとは、あるジェネレータのフローを別のジェネレータに挿入する自然な方法です。

 

中間結果を保存するために余分なメモリを使うこともありません。

 

"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

 

 

generator.next()の最初の呼び出しは、常に引数なしで行われなければなりません(引数が渡されても無視されます)。

 

これは実行を開始し、最初の降伏の結果 "2+2=? この時点で、ジェネレーターは行(*)に留まりながら、実行を一時停止します。

 

そして、上の図に示すように、収量の結果は、呼び出しコードの質問変数に入ります。

 

generator.next(4)では、ジェネレータが再開され、結果として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

 

 

  • 最初の.next()で実行を開始し、最初のイールドに到達します。
  • その結果は外部コードに返されます。
  • 2 番目の .next(4) は、最初の yield の結果として 4 をジェネレータに戻し、実行を再開します。
  • ...それは、ジェネレータ呼び出しの結果となる2つ目の降伏に到達します。
  • 3回目のnext(9)では、2回目の降伏の結果として9をジェネレータに渡し、関数の最後に到達した実行を再開します。

まるで「ピンポン」ゲームのようですね。各next(value)(最初のものを除く)は、現在の降伏の結果となる値をジェネレータに渡し、次の降伏の結果を返します。

 

ジェネレータースロー

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

...しかし、そこでエラーを発生させる(投げる)こともできます。エラーは結果の一種であるため、それは当然のことです。

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

例えば、ここでは "2 + 2 = ? "という収量がエラーを引き起こしています。

 

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)行目でジェネレーターに投げ込まれたエラーは、(1)行目でyieldによる例外が発生します。上の例では、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
}

 

ここでエラーをキャッチしないと、いつものように外部の呼び出しコード(もしあれば)に引き継がれ、キャッチされない場合はスクリプトが終了します。

 

この記事は、下記の英語の記事を参考に要点だけ翻訳してまとめたものです、詳しく知りたい方は本家の記事を読んでみてください。

 

Pocket
LinkedIn にシェア

React入門者へおすすめ動画&書籍おすすめ!

フロントエンド入門者へおすすめ動画&書籍おすすめ!

  • この記事を書いた人
  • 最新記事

藤沢瞭介(Ryosuke Hujisawa)

りょすけと申します。18歳からプログラミングをはじめ、今はフロントエンドでReactを書いたり、AIの勉強を頑張っています。off.tokyoでは、ハイテクやガジェット、それからプログラミングに関する情報まで、エンジニアに役立つ情報を日々発信しています!

-JavaScript

Copyright© off.tokyo , 2021 All Rights Reserved Powered by AFFINGER5.