2018/04/23 – 10 min read

ES6 では Generators(またはジェネレーター関数)という形で関数とイテレータを扱う新しい方法を紹介しました。 ジェネレーターは、途中で停止して、停止したところから継続できる関数です。 要するに、ジェネレーターは関数のように見えますが、イテレーターのように動作します。

Fun Fact: async/await はジェネレーターに基づくことができます。 詳しくはこちら。

Generator はイテレータと複雑にリンクしています。

技術的な詳細を説明する前に、ジェネレーターに対する直感を得るための簡単なアナロジーを紹介します。 本のページに夢中になっていると、ドアベルが鳴るのがほとんど聞こえません。 それはピザの配達員です。 あなたはドアを開けようと立ち上がりました。 しかし、その前に、最後に読んだページにしおりをセットする。 そして、そのプロットを記憶しておくのだ。 そして、ピザを取りに行く。 部屋に戻ったら、しおりをつけたページから本を読み始める。 もう最初のページからは読み始めない。

では、ジェネレータを利用して、プログラミングの際によくある問題を解決する方法を見ていきましょう。 しかし、その前に、ジェネレーターとは何かを定義しておきましょう。

function normalFunc() {
console.log('I')
console.log('cannot')
console.log('be')
console.log('stopped.')
}

normalFunc を終了する唯一の方法は、そこから return するか、throw エラーを発生させることです。

これに対して、ジェネレータは途中で停止して、停止したところから継続できる関数です。

  • ジェネレータは、イテレータを書く作業を単純化した関数の特別なクラスです。
  • ジェネレーターとは、単一の値ではなく一連の結果を生成する関数で、一連の値を生成します。 next()を呼び出すと、
    { 
    value: Any,
    done: true|false
    }

    という形のオブジェクトが返され、valueプロパティに値が格納される。 doneプロパティには、trueまたはfalseが入る。 donetrueになると、ジェネレータは停止し、それ以上の値は生成されません。

    以下は、同様の図です。

    Normal Functions vs Generators

    Generators 部分で Finish 直前の yield-resume-yield ループを閉じる点線矢印に注目してください。 ジェネレーターは決して終了しない可能性があります。

    ジェネレータの作成

    JavaScript でジェネレータを作成する方法を見てみましょう –

    function * generatorFunction() { // Line 1
    console.log('This will be executed first.');
    yield 'Hello, '; // Line 2 console.log('I will be printed after the pause');
    yield 'World!';
    }const generatorObject = generatorFunction(); // Line 3console.log(generatorObject.next().value); // Line 4
    console.log(generatorObject.next().value); // Line 5
    console.log(generatorObject.next().value); // Line 6// This will be executed first.
    // Hello,
    // I will be printed after the pause
    // World!
    // undefined

    太字部分に注目してください。 ジェネレーター関数を作成するために、単なる function ではなく function * 構文を使用します。 functionキーワード、*、関数名の間には、いくつでも空白を入れることができます。

    関数本体の内部には return はありません。 その代わりに、別のキーワードyieldがあります(2行目)。 これはジェネレータが自分自身を一時停止させることができる演算子です。 ジェネレータがyieldに遭遇するたびに、その後に指定された値を「返す」のです。 この場合、Hello,が返されます。 しかし、ジェネレータの文脈では「返される」とは言いません。 2899>

    ジェネレータから戻ることもできる。 しかし、returndoneプロパティをtrueに設定し、その後ジェネレータはそれ以上値を生成することができなくなります。 関数 generatorFunction を呼び出しているように見えます。 確かにそうです。 違いは、ジェネレータ関数は値を返す代わりに、常にジェネレータオブジェクトを返すことです。 ジェネレータ・オブジェクトはイテレータです。

    4 行目では、generatorObjectnext() メソッドを呼び出しています。 この呼び出しで、ジェネレータは実行を開始します。 まず、This will be executed first.console.logし、yield 'Hello, 'に遭遇する。 ジェネレータはその値をオブジェクト{ value: 'Hello, ', done: false }として生成し、一時停止する。 2899>

    5行目で再びnext()を呼び出している。 今度はジェネレータが目を覚まし、出発したところから実行を始める。 次に見つかった行はconsole.logである。 文字列 I will be printed after the pause をログに記録しています。 さらにyieldが見つかりました。 値はオブジェクト { value: 'World!', done: false } として出力されます。 value プロパティを抽出し、それをログに記録します。 2899>

    6行目で再びnext()を呼び出しています。 今回はもう実行する行はない。 どの関数もreturn文が提供されないと暗黙のうちにundefinedを返すことを思い出してください。 したがって、ジェネレータはオブジェクト{ value: undefined, done: true}を(yieldの代わりに)返している。 done には true がセットされる。 これはこのジェネレータの終わりを告げるものである。

    ジェネレーターを再び実行するには、新しい別のジェネレーター オブジェクトを作成する必要があります。

    イテレータブルの実装

    イテレータを実装する場合、next()メソッドを持つイテレータオブジェクトを手動で作成する必要があります。 また、手動で状態を保存する必要があります。 多くの場合、それを行うのは本当に難しくなります。 ジェネレータはイテレータブルでもあるので、余分な定型的なコードなしにイテレータブルを実装するのに使うことができます。

    問題: Thisisiterable. を返すカスタムの反復処理関数を作成したい。 以下はイテレータを使った実装です –

    const iterableObj = {
    () {
    let step = 0;
    return {
    next() {
    step++;
    if (step === 1) {
    return { value: 'This', done: false};
    } else if (step === 2) {
    return { value: 'is', done: false};
    } else if (step === 3) {
    return { value: 'iterable.', done: false};
    }
    return { value: '', done: true };
    }
    }
    },
    }for (const val of iterableObj) {
    console.log(val);
    }// This
    // is
    // iterable.

    ジェネレータを使った同じものです –

    function * iterableObj() {
    yield 'This';
    yield 'is';
    yield 'iterable.'
    }for (const val of iterableObj()) {
    console.log(val);
    }// This
    // is
    // iterable.

    両方のバージョンを比較してみてください。 確かに、これはある種の作為的な例です。

    • Symbol.iterator
    • next().
    • を実装する必要はなく、手動で next() の戻りオブジェクト、つまり { value: 'This', done: false }.

    • を作る必要はなく、状態を保存しなくて良い。 イテレータの例では、状態は変数 step に保存されていた。 その値がイテレータブルから出力されるものを定義していた。

    Better Async functionality

    function fetchJson(url) {
    return fetch(url)
    .then(request => request.text())
    .then(text => {
    return JSON.parse(text);
    })
    .catch(error => {
    console.log(`ERROR: ${error.stack}`);
    });
    }

    のような約束とコールバックを使用するコードは、(co.js などのライブラリの助けを借りて)

    const fetchJson = co.wrap(function * (url) {
    try {
    let request = yield fetch(url);
    let text = yield request.text();
    return JSON.parse(text);
    }
    catch (error) {
    console.log(`ERROR: ${error.stack}`);
    }
    });

    と書くことができ、async/await を使うことと類似していると気付いた読者がいるかもしれない。 これは偶然の一致ではありません。 async/await は同様の戦略をとることができ、約束が関係する場合には yield を await に置き換えることができる。 ジェネレータに基づくことができる。 2899>

    Infinite Data Streams

    決して終わらないジェネレータを作成することが可能です。

    function * naturalNumbers() {
    let num = 1;
    while (true) {
    yield num;
    num = num + 1
    }
    }const numbers = naturalNumbers();console.log(numbers.next().value)
    console.log(numbers.next().value)// 1
    // 2

    ジェネレータ naturalNumbers を作成します。 この関数の内部で、while無限ループが発生します。 そのループの中で、numyieldします。 ジェネレータが降伏すると、中断される。 再びnext()を呼び出すと、ジェネレータは目を覚まし、中断していた場所(この場合はyield num)から継続し、別のyieldに遭遇するかジェネレータが終了するまで実行される。 次の文はnum = num + 1であるので、numを更新する。 そして、whileループの先頭に行く。 条件はまだ真である。 次の行 yield num に進む。 更新されたnumを出力し、中断する。

    オブザーバとしてのジェネレータ

    ジェネレータもnext(val)関数を使って値を受け取ることができる。 そして、ジェネレータは新しい値を受け取ったときに目を覚ますので、オブザーバと呼ばれる。 ある意味、それは値のために観察し続け、1つを得るとき作用する。 このパターンについて詳しくは、こちらを参照してください。

    ジェネレーターの利点

    Infinite Data Streams の例で見られるように、これは遅延評価によってのみ可能である。 遅延評価とは、値が必要になるまで式の評価を遅らせる評価モデルです。 つまり、値が必要でなければ存在しないことになります。 要求に応じて計算される。 例を見てみよう –

    function * powerSeries(number, power) {
    let base = number;
    while(true) {
    yield Math.pow(base, power);
    base++;
    }
    }

    powerSeriesは、ある数を累乗にした級数を与える。 例えば、3の2乗の級数は、9(3²) 16(4²) 25(5²) 36(6²) 49(7²)となります。 const powersOf2 = powerSeries(3, 2);を実行すると、ジェネレータオブジェクトを作成するだけです。 どの値も計算されていません。

    Memory Efficient

    Lazy Evaluation の直接的な結果は、ジェネレータがメモリ効率的であることです。 私たちは必要な値のみを生成します。 通常の関数では、すべての値を事前に生成し、後で使用する場合に備えてそれらを保持しておく必要がありました。 しかし、ジェネレーターを使用すると、必要なときまで計算を延期できます。

    ジェネレーターで動作するコンビネーター関数を作成することができます。 コンビネータとは、既存の反復記号を組み合わせて新しい反復記号を作成する関数である。 これは反復記号の最初のn要素を取る。

    function * take(n, iter) {
    let index = 0;
    for (const val of iter) {
    if (index >= n) {
    return;
    }
    index = index + 1;
    yield val;
    }
    }

    take の興味深い使用例をいくつか紹介します。

    take(3, )// a b ctake(7, naturalNumbers());// 1 2 3 4 5 6 7take(5, powerSeries(3, 2));// 9 16 25 36 49

    cycled library の実装(反転機能なし)です。 一度すべての値を使い切ると、再び反復することはできません。 値を再び生成するには、新しいジェネレータオブジェクトを作る必要があります。

    const numbers = naturalNumbers();console.log(...take(10, numbers)) // 1 2 3 4 5 6 7 8 9 10
    console.log(...take(10, numbers)) // This will not give any data
    • ジェネレータオブジェクトは、配列で可能なランダムアクセスを許しません。 値は1つずつ生成されるので、ランダムな値にアクセスすると、その要素までの値を計算することになります。

    結論

    ジェネレーターでは、まだ多くのことがカバーされていません。 yield *, return(), throw() のようなものです。 ジェネレータはまた、コルーチンを可能にします。

    Python の itertools ページに移動して、イテレータとジェネレータで作業できるユーティリティのいくつかを見ることができます。 練習として、あなた自身でユーティリティを実装することができます。

admin

コメントを残す

メールアドレスが公開されることはありません。

lg