JavaScriptの発電機



オープニングノート
ハローフェロープログラマー👋 この記事では、私たちは、ES 6で導入されたJavaScriptの発電機の基礎を歩き、実際のユースケースに遭遇します.

発電機とは何か
その名前から、ジェネレータは複数の呼び出しの向こう側にその状態(文脈)を保存している間、実行手順を終了して、再入力することによって1つ以上の値を生成することができる機能です.簡単な単語にするには、ジェネレータは通常の関数と似ていますが、以前の状態を保存するだけで、以前に終了した時点で実行を継続する能力を持ちます.以下のフローチャートは、通常機能と発電機機能との違いを示す.


構文
既に推測しているように、通常の関数とジェネレータの間にいくつかの構文上の違いがあります.
// Normal Function
function normalFunction(params) {
  // your logic goes here
  return value;
}

/* --------------------------------- */

// Generator Function
function* generatorFunction(params) {
  // your logic
  yield value1;

  // your logic
  yield value2;

  /*
    .
    .
    .
  */

  // your logic
  yield valueN;
}

構文の最初の顕著な違いは、ジェネレータがfunction* の代わりにキーワードfunction . また、どのように我々はreturn キーワードは、通常の関数では、我々はyield 代わりにジェネレータ関数のキーワード.The yield ジェネレータ内のキーワードでは、値を' return 'し、実行を終了し、現在の辞書スコープの状態(コンテキスト)を保存し、最後の終了点で実行を再開するのを待機します.
注意:通常の関数では、return キーワードを一度、値を返し、関数を完全に終了します.ジェネレータでは、yield あなたが連続した呼び出しに'戻り値'値をしたいだけ多くのキーワード.また、return ジェネレータ内のキーワードですが、別の日にこの議論を残してください.

呼び出し
両方の関数間の構文の違いをカバーしたので、どのようにジェネレータを呼び出し、その値をyieldするかを見てみましょう.まず、通常の関数の呼び出しを示す次のコードを考えます.
function normalFunction() {
  console.log('I have been invoked');
}

// invocation
normalFunction();
一般的に、関数のシグネチャを入力することで、通常の関数を呼び出すことができます() . 前のコードが出力されます.
I have been invoked
では、同じプロシージャを使用してジェネレータを呼び出します.次のコードを厳密に検査します.
function* generatorFunction() {
  console.log('I have been invoked');
  yield 'first value';

  console.log('resuming execution');
  yield 'second value';
}

// does this invoke the generator?
generatorFunction();

あなたはそのようなプログラムから何を期待していますか.技術的には、最初のyieldキーワードを打つまで、関数を実行することを期待します.しかし、前のプログラムの出力は空だった.

これは、通常の起動構文が実際にジェネレータ関数の本体を実行しないためです.代わりにGenerator 複数のプロパティとメソッドを保持するオブジェクトです.これを証明するために、我々はプリントアウトしようとすることができますconsole.log(generatorFunction()) 出力は以下のようになります.
Object [Generator] {}
だから、問題はどのように、我々は実際に発電機から我々の価値を生じますか?
さて、それに属するいくつかの重要な方法がありますGenerator 我々が利用できるオブジェクト.最も重要な方法はnext() , その名前から、定義されたジェネレータから次の値を得ます.では、以前のコードを変更して、実際に値をyieldします.
function* generatorFunction() {
  console.log('I have been invoked');
  yield 'first value';

  console.log('resuming execution');
  yield 'second value';
}

// store the Generator Object in a variable
let foo = generatorFunction();

// execute until we yield the first value
console.log(foo.next());

// resume execution until we yield the second value
console.log(foo.next());

// execute until the function ends
console.log(foo.next());

前のコードの出力は以下の通りです.
I have been invoked
{ value: 'first value', done: false }
resuming execution
{ value: 'second value', done: false }
{ value: undefined, done: true }

出力線を一行ずつ検査しましょう.最初に呼び出すときfoo.next() メソッドは、最初のyieldキーワードを押すまで実行を開始し、実行を停止します.これは出力の最初の2行に反映されます.注意foo.next() 返還するObject 実際の値の代わりに.このオブジェクトには、必ず次のプロパティが含まれます.
  • '値':ジェネレータから現在の値を保持します.
  • '「完了」:ジェネレータ実行が終わりに達したかどうかを示すブールフラグ.
  • 二番目に移るfoo.next() コール.予想通り、ジェネレータは最後の終了ステップから実行を再開して、それが第2の降伏キーワードを打つまで実行する.そして、それは出力の三分の一および第4のラインに反映される.注意done フラグはまだfalse , 関数の最後に到達しなかったので.
    最後にfoo.next() コールすると、関数は2番目のyieldキーワードの後に実行を再開し、実行するための何も見つけません.この時点では、より多くの値をyieldするとdone フラグがtrue 出力の最後の行に反映されます.
    JavaScriptのジェネレータの基本的な概念をカバーしてきたので、便利なユースケースのいくつかを見てみましょう.

    ユースケース

    ユースケース1:模倣するrange() Pythonからの関数
    Pythonドキュメントによるとrange typeは不変の数列を表し、forループ内の特定の回数をループするために一般的に使用されます.「range() Pythonでの関数は、通常以下のパラメータを含みます:
  • start (オプションでは、デフォルト= 0 ):シーケンス内の最初の数を含みます.
  • end (必須):シーケンスの最後の数、排他的.
  • step (オプションでは、デフォルト= 1 ) :シーケンス内の任意の2つの数字の違い.
  • 基本的にはrange() Pythonでの機能は以下の通りです.
    # Python code
    for i range(3):
        print(i)
    
    # output:
    # 0
    # 1
    # 2
    
    
    我々がする必要があるのは、JavaScriptでこの機能を模倣することです.次のコードを厳密に検査します.
    /*
    range function implemented in Javascript
    */
    function* range({start = 0, end, step = 1}) {
      for (let i = start; i < end; i += step) yield i;
    }
    
    一歩一歩それをしましょう.まず、関数シグネチャは、3つの引数をとるジェネレータを定義します.start , end and step , その中でstart and step に失敗する0 and 1 それぞれ.関数本体に移動すると、ループから始まるstart 包括的なまでend 排他的.ループの範囲内で、我々は値を得ますi を返します.
    アクションでそれを見ることができます.コードの次のコードは、実装の異なる例を示しますrange 機能
    // first example
    for (let i of range({end: 4})) console.log(i);
    
    /*
    output:
    0
    1
    2
    3
    */
    
    // second example
    for (let i of range({start: 2, end: 4})) console.log(i);
    
    /*
    output:
    2
    3
    */
    
    // third example
    for (let i of range({start: 1, end: 8, step: 2})) console.log(i);
    
    /*
    output:
    1
    3
    5
    7
    */
    
    

    ユースケース2:バブルソートアルゴリズムの可視化
    このユースケースでは、与えられた配列のバブルソートアルゴリズムの段階的実行を簡単に可視化するために、出力を試みます.簡単に、バブルソートは次のように動作します長さの配列を与えられますn and i 現在の反復としてmax(array[0:n - i]) インデックスにn - i 配列がソートされるまで繰り返します.デフォルトの実装は以下の通りです.
    /*
    Bubble Sort implementation in javascript
    */
    function bubbleSort(arr) {
      for (let i = arr.length - 1; i >= 0; i--) {
        for (let j = 0; j < i; j++) {
          // if the current value is larger than its adjacent
          // swap them together
          if (arr[j] > arr[j+1]) {
            [arr[j], arr[j+1]] = [arr[j+1], arr[j]];
          }
        }
      }
    
      return arr;
    }
    
    私たちの仕事は、このアルゴリズムを通して実行されている段階的な比較とスワップを視覚化することです.これは簡単にジェネレータを使用して行うことができます.内部ループ内の各反復の後に、単に現在の配列を生成します.新しい関数は次のようになります.
    /*
    visualize Bubble Sort implementation in javascript
    */
    function* visualizeBubbleSort(arr) {
      for (let i = arr.length - 1; i >= 0; i--) {
        for (let j = 0; j < i; j++) {
          if (arr[j] > arr[j + 1]) {
            [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
          }
    
          yield arr;
        }
      }
    }
    
    これは内部ループ内の反復処理ごとに配列を生成し、配列の現在の状態を示します.次の例を考えます.
    let inputArray = [40, 30, 2, 20];
    let currentStep = 1;
    for (let val of visualizeBubbleSort(inputArray)) {
      console.log(`step #${currentStep}: [${val}]`);
      currentStep++;
    }
    
    前のプログラムの出力は次のようになります.
    step #1: [30,40,2,20]
    step #2: [30,2,40,20]
    step #3: [30,2,20,40]
    step #4: [2,30,20,40]
    step #5: [2,20,30,40]
    step #6: [2,20,30,40]
    
    実装されているジェネレータのおかげで、アルゴリズム全体で何が起こっているかをはっきりと見ることができます.
  • ステップ1 ->スワップ40 with 30
  • ステップ2 ->スワップ40 with 2
  • ステップ3 ->スワップ40 with 20
  • ステップ4 ->スワップ30 with 2
  • ステップ5 ->スワップ30 with 20
  • ステップ6 ->何も交換しないで、配列をソートする
  • 注:このテクニックは簡単に任意のアルゴリズムを視覚化するために使用することができます.それは時々非常に役立つことができます.

    ユースケース3:需要に応じて異なる乱数を生成する
    このユースケースでは、ジェネレータを使用して一連の異なる乱数を生成しようとします.まず、入力と出力に制約を与えます.
  • この関数は、正の整数のみを生成する必要があります.
  • この関数はパラメータlimit , これは生成された整数の最大数と最大生成可能な整数を決定します.
  • この関数は、有効な整数のプールを選択して格納する方法を持たなければなりません.
  • 以前の制約に従い、簡単にジェネレータを使用してこの機能を実装できます.
    /*
    distinctRandom implementation in js 
    */
    function* distinctRandom({limit = 10}) {
      // we create an array that contains all numbers in range [0:limit)
      // this is our initial pool of numbers to choose from
      const availableValues = [...new Array(limit)].map((val, index) => index);
    
      // we repeatedly loop until the available pool of numbers is empty
      while (availableValues.length !== 0) {
        // generate a random index in range [0: availableValues.length)
        // then, yield the number that is present at the chosen index
        // Finally, remove the picked item from the pool of available numbers
        const currentRandom = Math.floor(Math.random() * availableValues.length);
        yield availableValues[currentRandom];
        availableValues.splice(currentRandom, 1);
      }
    }
    
    簡単に言えば、前のジェネレータは利用できる整数のプールを維持しようとします.各反復では、ランダムにこのプールから番号を選択し、それをyieldし、有効なプールから削除します.理論的には、生成された整数の最大数はlimit そして、生成された整数はすべて異なっていなければなりません.実行終了時まで実装された発電機を使い果たすことによって容易に証明できます.
    // we set the limit to 8
    for (const val of distinctRandom({limit: 8})) {
      console.log(val);
    }
    
    /*
    sample output:
    3
    7
    5
    2
    4
    0
    1
    6
    */
    

    クロージングノート
    ジェネレータは、複数の問題とユースケースのソリューションを提供するES 6への大きな追加です.あなたは確かにどこでもそれらを使用することができますが、私たちは、彼らが時代にデバッグするのは難しいことができるだけでなく、あなたのコードにより複雑さを導入することができるように、発電機のために解決する前に、手の問題の別の解決策を検討することをお勧めします.にもかかわらず、ハッピーコーディング🎉