関数ベースのイテレーターとジェネレーター

言語コンストラクトとしてのイテレーターとジェネレーターのサポートは、バージョン1.7でのみJavaScriptに登場し、ブラウザーでこれらのすばらしいものを長い間使用することを夢見ているだけです。 ただし、現在はJavaScriptでデザインパターンの形式でイテレータとジェネレータを使用できます。さらに、これは非常に簡単であり、場合によってはさらに便利です。



私のトピックでは、「実際のイテレーター」を頻繁に参照します。これは、 Iterators&Generatorsトピックで完全に照らされたazproductionとまったく同じjavascript 1.7のイテレーターを意味します。

比較のために彼の「実際の」例のコードを使用するので、彼のトピックに精通することを強くお勧めします。



さあ、行こう!



簡単な例



オブジェクトの特定のセットがあります。それらをユニットと呼びましょう。 すべてのユニットに、セット[赤、緑、青]からの色を周期的な順序で設定する必要があります。 つまり、最初のユニットは赤、2番目は緑、3番目は青、4番目は再び赤というように続きます。



これは、通常Revolverと呼ばれるヘルパーオブジェクトで実現されます。



var colors = new Revolver(['red', 'green', 'blue']); for (var i = 0; i < units.length; i += 1) { units[i].color = colors.next(); }
      
      





リボルバーは次のとおりです。



 function Revolver(items) { this.items = items; this.max = items.length - 1; this.i = -1; } Revolver.prototype.next = function () { this.i = this.i < this.max ? this.i + 1 : 0; return this.items[this.i]; };
      
      





私はこのコードが好きではありません。 なぜだろう? 私は答えます:ジェスチャーが多すぎます。 ユニットを色付けするためだけに、自分で確認してください:

  1. ヘルパークラスを宣言する
  2. このクラスでnext()メソッドを定義し、
  3. 目的の値のセットでクラスのインスタンスを作成し、
  4. インスタンスのnext()メソッドを呼び出して、セットから次の値を取得します。


コードが小さくなり、意味がより大きくなるようにRevolverを単純化することは可能ですか?

できます! 関数を使用、ルーク!



 function revolver(items) { var max = items.length - 1, i = -1; return function () { i = i < max ? i + 1 : 0; return items[i]; }; } var next_color = revolver(['red', 'green', 'blue']); for (var i = 0; i < units.length; i += 1) { units[i].color = next_color(); }
      
      





現在、メソッドを持つクラスはありませんが、代わりに関数を返す関数があります。

実際にはオブジェクトの形の色のセットは必要ではなく、セットから次の色を取得する方法が必要なため、意味の集中は最大です。



コレクションがない場合にコレクション要素を取得する方法は、イテレータと呼ばれます。



理論



イテレータは、プログラマが実装の機能を考慮せずにコレクションのすべての要素を反復処理できるようにするオブジェクトです(Wikipedia)。



与えられた例では、コレクションはr、g、b、r、g、b、...の色の無限の繰り返しセットであり、コレクションの機能は本当に隠されています。



イテレータはジェネレータによって生成されます。 ジェネレーターは、前のリターンがどこにあったかを覚えている関数のように見え、次の呼び出しで、中断された場所から作業を再開します。 ジェネレーターは、特定のタイプのイテレーターのファクトリーです;ジェネレーターが呼び出された後、ジェネレーターによって作成された各イテレーターは、独自の独立したライフを持​​ちます。



この例では、リボルバー関数は、送信された要素セットからの無限シーケンス反復子のジェネレーターです。



したがって、関数イテレータとジェネレータは次のように定義できます。
  1. イテレータは、連続した呼び出しでコレクションの要素を返す関数です。
  2. ジェネレータは、イテレータ関数を返す関数です。


ジェネレーターの伝統的な使用例は、フィボナッチ数列などの無限数列の生成です。 機能ジェネレータとしてどのように実装できるか見てみましょう。



 function fibonacci() { var fn1 = 1, fn2 = 1; return function () { var current = fn1; fn1 = fn2; fn2 = fn2 + current; return current; }; } var sequence = fibonacci(); for (var i = 0; i < 5; i += 1) { console.log(sequence()); // 1, 1, 2, 3, 5 }
      
      





うまくいく! このジェネレーターのコードと比較してください:



 function fibonacci(){ var fn1 = 1, fn2 = 1; while (1) { var current = fn1; fn1 = fn2; fn2 = fn2 + current; yield current; } }
      
      





そうですね。



練習する



イテレータの目的の1つは、インターフェイスを表すコレクションをループ処理することです。



実際のイテレータはfor iループ文にうまく適合します。 コレクションの終わりを示すために、イテレーターは特別な例外をスローします。これはforステートメントがループからの終了シグナルとして理解するものです。



明らかに、関数イテレータの動作方法は異なるはずです。



最初に決定することは、コレクションの終了の指定です。 これを行うには、javascript関数の次のプロパティを使用すると便利です。関数本体の終了前にreturnステートメントが満たされない場合、関数実行の結果は未定義になります。 この機能により、イテレータコードをより理解しやすく、読みやすくすることができます。コレクションに要素がある限り、戻り値で要素を返します。要素が終了しても、何も返しません。



2番目は、関数イテレータを反復処理できる構造です。 この設計は、イテレーターだけでなく、ジェネレーターにも必要です。 ここのすべても非常に簡単です:



 //  — while var item; while (item = iterator()) { //  item } //  — for for (var item, iter = generator(); item = iter();) { //  item }
      
      





サイクルを整理する例:Nを超えない2のべき乗のジェネレータ。



 function powers_of_two(N) { var value = 1; return function () { var result = value; value *= 2; if (result < N) { return result; } }; } for (var p, iter = powers_of_two(42); p = iter();) { console.log(p); // 1, 2, 4, 8, 16, 32 }
      
      







軟膏で飛ぶ、それなしで


実際のジェネレーターには、最後のyieldステートメントの呼び出しポイントからイテレーター本体の実行を再開するという重要な特性があります。 この機能により、線形コードを使用して、通常ループまたは再帰によって生成される要素のシーケンスを記述できるため、コードが簡素化されます。



良い例は、 Iterators&Generatorsトピックのツリートラバーサルジェネレーターです



 function inorder(t) { if (t) { for (var item in inorder(t.left)) { yield item; } yield t.label; for (var item in inorder(t.right)) { yield item; } } }
      
      





明快で論理的に見えます。左ブランチの要素、ルート、右ブランチの要素を返します。 クラス。



関数ジェネレーターは、returnステートメントの後、コードの実行を続行できません;各イテレーター呼び出しは、最初から関数の実行につながります。 したがって、状態は反復関数によって閉じられた変数に格納する必要があります。



機能的な再帰的なツリー走査ジェネレーターを作成してみましょう。

試行1、実際のジェネレーターのロジックの複製:



 function inorder(t) { var root = false, left, right; if (t) { left = inorder1(t.left); right = inorder1(t.right); return function () { var item; if (item = left()) { return item; } if (!root) { root = true; return t.label; } if (item = right()) { return item; } }; } else { return function () {}; } }
      
      





それはまあまあ判明しましたが、2つのことが特に悲しいです。



1.イテレーターの本体に3つのreturnステートメントが存在する。 yieldステートメントの場合、実行は次の行から継続するため、すべてが論理的に見えます。 ただし、returnがyieldとして使用される場合、ロジックが壊れ、何が起こっているのかを理解することがより難しくなります。



2.ジェネレーターの入力にある空のツリーは、別の空のイテレーター戻り関数(){}によって処理されます。これにより、コードがさらに混乱します。



この場合の問題の原因は、状態を維持する方法を知っている実際のジェネレーターのロジックをコピーすることです。 関数発生器のコードを理解できるようにするには、状態をより明示的に保存する必要があります。



ツリーでの反復について熟考します。

明らかに、ツリーの左と右のブランチのノード上でイテレータを操作する必要があります。

左ブランチイテレータが使い果たされたら、ルートを使用してから、右ブランチイテレータを使用する必要があります。

ルートをイテレータとして想像できる場合、3つのイテレータを取得します。各イテレータは、最後まで順番に使用し、結果を返し、その後、ツリーを完全にトラバースします。



コードに実装してみましょう:

 function inorder(t) { var roots = [t], //     iters = []; if (t) { iters.push(inorder(t.left)); iters.push(function () {return roots[0] && roots.shift().label}); //   iters.push(inorder(t.right)); } return function () { var leaf; while (iters.length) { leaf = iters[0](); if (leaf) { return leaf; } iters.shift(); } }; }
      
      





それはほとんどうまくいきました-イテレーターのreturnステートメントは1つで、イテレーターのコードは非常に単純です。



したがって、複雑なロジックを持つ関数ジェネレーターは非常に実現可能ですが、実際のジェネレーターよりも慎重に検討する必要があります。



イテレーター管理



たとえば、次のように、トピックの冒頭で特に細心の注意を払っている人が反対する場合があります。「ちょっと待ってください、イテレータは関数です。 「すばらしいですが、イテレータの状態をリセットする必要がある場合、この関数で何をしますか?」



答えは次のとおりです。この関数にメソッドを追加します:) javascriptの関数はオブジェクトです。 もちろん、関数にはメソッドがあります。



リセットメソッドを使用した、古き良きフィボナッチ数列:



 function fibonacci_restartable() { var fn1 = 1, fn2 = 1, iterator; iterator = function () { var current = fn1; fn1 = fn2; fn2 = fn2 + current; return current; }; iterator.restart = function () { fn1 = fn2 = 1; }; return iterator; } var sequence = fibonacci_restartable(); for (var i = 0; i < 5; i += 1) { console.log(sequence()); // 1, 1, 2, 3, 5 } sequence.restart(); for (var i = 0; i < 5; i += 1) { console.log(sequence()); // 1, 1, 2, 3, 5 }
      
      





用語を使用するために、再起動メソッドはprivilegedであり、実際には反復子の内部変数への短絡があることを意味します。

同様に、イテレータには、その内部状態に影響を与える必要なメソッドを追加できます。



結論



設計パターンの形式のイテレーターとジェネレーターは、その機能の柔軟性により、javascriptにかなり定着しています。 もちろん、関数イテレータは実際のイテレータよりもパフォーマンスが劣りますが、パフォーマンスが重要でない場所では、関数イテレータを使用するとコードを合理化および簡素化できます。



ご清聴ありがとうございました!



All Articles