JavaScriptのトランスデューサー。 パート2

最初の部分では、次の仕様に焦点を当てました。トランスデューサーは、 step



関数を取り、新しいstep



関数を返す関数です。



 step⁰ → step¹
      
      





step



関数は、現在の結果と次の要素を取得し、新しい現在の結果を返す必要があります。 さらに、現在の結果のデータ型は指定されていません。



 result⁰, item → result¹
      
      





step¹



関数で新しい現在の結果を取得するには、 step¹



関数を呼び出して、古い現在の結果とそれに追加する新しい値を渡す必要があります。 値を追加したくない場合は、単純に古い結果を返します。 1つの値を追加する場合は、 step⁰



を呼び出します。 step⁰



値が返されるという事実が新しい結果として返されます。 複数の値を追加する場合は、チェーンに沿ってstep⁰



何度か呼び出します。これは、 フラット化トランスデューサーの実装例を使用すると簡単にわかります。



 function flatten() { return function(step) { return function(result, item) { for (var i = 0; i < item.length; i++) { result = step(result, item[i]); } return result; } } } var flattenT = flatten(); _.reduce([[1, 2], [], [3]], flattenT(append), []); // => [1, 2, 3]
      
      





つまり step



数回呼び出す必要があり、そのたびに現在の結果が変数に保存され、次の呼び出しで渡され、最後に最後の結果が返されます。



その結果、各要素を処理するときに、あるstep



関数が別のstep



関数を呼び出し、最後のユーティリティ関数step



がコレクションに結果を保存するまで(最初の部分からappend



、というように続きappend







そのため、次のことができます。

  1. 要素の変更(ノートマップ)
  2. アイテムをスキップ(約フィルター)
  3. 1つの要素に対して複数の新しいものを発行します(約フラット化)




早期終了



しかし、途中でプロセス全体を中断したい場合はどうでしょうか? つまり たとえば、 takeを実装します 。 これを行うために、Richは戻り値を特別な「縮小」ラッパーでラップすることをお勧めします。



 function Reduced(wrapped) { this._wrapped = wrapped; } Reduced.prototype.unwrap = function() { return this._wrapped; } Reduced.isReduced = function(obj) { return (obj instanceof Reduced); } function take(n) { return function(step) { var count = 0; return function(result, item) { if (count++ < n) { return step(result, item); } else { return new Reduced(result); } } } } var first5T = take(5);
      
      





プロセスを完了したい場合は、通常のように次のresult



を返す代わりに、 Reduced



ラップされたresult



を返します。 ステップ関数の署名をすぐに更新します。



 result⁰, item → result¹ | reduced(result¹)
      
      





ただし、 _.reduce



関数で_.reduce



、このようなバージョンのトランスデューサーを処理できなくなります。 新しいものを書く必要があります。



 function reduce(coll, fn, seed) { var result = seed; for (var i = 0; i < coll.length; i++) { result = fn(result, coll[i]); if (Reduced.isReduced(result)) { return result.unwrap(); } } return result; }
      
      





これで、 first5T



トランスデューサーを適用できます。



 reduce([1, 2, 3, 4, 5, 6, 7], first5T(append), []); // => [1, 2, 3, 4, 5]
      
      







また、stepを数回呼び出すトランスデューサーにReduced.isReduced(result)



チェックを追加するReduced.isReduced(result)



があります(約Flatten)。 つまり 再度ステップ呼び出しでフラット化すると、Reducedにラップされた結果が返されます。サイクルを完了し、このラップされた結果を返す必要があります。



状態



もう1つの重要な詳細、テイクトランスデューサーには状態があります。 彼はすでにいくつの要素が彼を通過したかを覚えています。 すべてが正しく機能するために、このカウンターは、例で作成された場所(var countを参照)で正確に作成する必要があります。 stepを返す関数内。 たとえば、グローバル変数である場合、1つのカウンターですべてのタイプのトランスデューサーのエレメントをカウントし、間違った結果を取得します。



状態が作成された瞬間を明確に示すために、トランスデューサーを起動する別のユーティリティ関数を作成しましょう。



 function transduce(transducer, append, seed, coll) { var step = transducer(append); //       . // step    , //   (step)      //    ,   . return reduce(coll, step, seed); } transduce(first5T, append, [], [1, 2, 3, 4, 5, 6, 7]); // => [1, 2, 3, 4, 5]
      
      







完了



早期終了については既に説明しましたが、元のコレクションが終了するだけで正常終了する場合があります。 一部のトランスデューサーは、何らかの方法で完了を処理する場合があります。



たとえば、コレクションを特定の長さの小さなコレクションに分割したいが、最後の小さなコレクションに十分な要素がない場合は、不完全なコレクションを返すだけです。 これ以上要素がなくなることを何らかの形で理解し、何が返されるかが必要です。



Richがこれを行うことができるように、彼は次の値は渡されず、現在の結果のみが転送されるステップ関数の別のバージョンを追加することを提案します。 早期終了がなかった場合、このオプションは収集処理の終了時に呼び出されます。



clojureでは、これら2つの関数は1つに結合されていますが、JavaScriptでもこれを行うことができます。



 function step(result, item) { if (arguments.length === 2) { //   //  step(result, item)     } if (arguments.length === 1) { //   //    step c  ,     . //     -     , //      step   ,    . //    return step(result); // -  result = step(result, -); return step(result); } }
      
      





ステップ関数のシグネチャを更新します。引数の数に応じて2つのオプションがあります。



 result⁰ → result¹ * result⁰, item → result¹ | reduced(result¹) *        reduced(result¹),      .      .
      
      







すべてのトランスデューサは、通常のステップと最終呼び出しの両方の操作をサポートする必要があります。 また、 transduce()



およびappend()



関数を更新して、最終呼び出しのサポートを追加する必要があります。



 function transduce(transducer, append, seed, coll) { var step = transducer(append); var result = reduce(coll, step, seed); return step(result); } function append(result, item) { if (arguments.length === 2) { return result.concat([item]); } if (arguments.length === 1) { return result; } }
      
      







パーティションの実装は次のとおりです(コレクションを小さなコレクションに分割します)。



 function partition(n) { if (n < 1) { throw new Error('n     1'); } return function(step) { var cur = []; return function(result, item) { if (arguments.length === 2) { cur.push(item); if (cur.length === n) { result = step(result, cur); cur = []; return result; } else { return result; } } if (arguments.length === 1) { if (cur.length > 0) { result = step(result, cur); } return step(result); } } } } var by3ItemsT = partition(3); transduce(by3ItemsT, append, [], [1,2,3,4,5,6,7,8]); // => [[1,2,3], [4,5,6], [7,8]]
      
      







初期化



また、Richは、トランスデューサーに初期の空の結果値を作成する機能を追加することを提案します。 (これらの目的にはどこでも空の配列を使用しました。これは最初にreduce



に、次にtransduce



に明示的に渡されました。)



これを行うには、ステップ関数の別のバリアントを追加する必要があります-パラメータなしで。 stepがパラメーターなしで呼び出された場合は、空の配列などの初期値を返す必要があります。



処理中のコレクションのタイプに関連付けられていないため、トランスデューサーが空の配列を作成できないことは明らかです。 ただし、トランスデューサーのステップ関数に加えて、外部関数ステップもあります。これは、収集のタイプを知っています。 この例では、これは追加機能です。



step



関数の署名を更新します。



 → result result⁰ → result¹ result⁰, item → result¹ | reduced(result¹)
      
      





transduce()



およびappend()



関数を更新する



 function transduce(transducer, append, coll) { var step = transducer(append); var seed = step(); var result = reduce(coll, step, seed); return step(result); } function append(result, item) { if (arguments.length === 2) { return result.concat([item]); } if (arguments.length === 1) { return result; } if (arguments.length === 0) { return []; } }
      
      





また、たとえば、マップトランスデューサージェネレーターを書き換えます。



 function map(fn) { return function(step) { return function(result, item) { if (arguments.length === 2) { return step(result, fn(item)); } if (arguments.length === 1) { return step(result); } if (arguments.length === 0) { return step(); } } } }
      
      





append()



内のtransduce()



パラメーターから空の配列を移動しただけで、一見するとこれは不要なアクションですが、コレクションの先頭に何かを追加するトランスデューサーを作成する機会が与えられました(末尾に追加するものなど) 。



したがって、すべてのトランスデューサーは、ステップ関数で3つの操作(通常のステップ、最終呼び出し、および初期呼び出し)をサポートする必要があります。 しかし、それらのほとんどは、最後の2つのケースでイニシアチブを次のトランスデューサーに単純に転送します。



まとめ



それだけです リッチヒッキーの報告書全体を再び語った。 そして、私が理解するように、これはトランスデューサーについて言えることのすべてです。



私たちが得たものをもう一度要約します。 コレクションの操作を作成する普遍的な方法がありました。 これらの操作では、要素の変更(マップ)、要素のスキップ(フィルター)、要素の伝播(フラット化)、状態の取得(テイク、パーティション)、処理の早すぎる完了(テイク)、最後に何かを追加(パーティション)、次に最初に。 composeを使用してこれらすべての操作を簡単に結合し、FRPなどの通常のコレクションで両方を使用できます。 さらに、これらはすべて迅速に機能し、メモリをほとんど消費しません。 一時的なコレクションは作成されません。



これはすべてクールです! しかし、どのようにそれらを使用し始めますか? 問題は、トランスデューサーを最大限に使用するために、JavaScriptコミュニティが仕様に同意する必要があることです(そうすることはできますか?:-)。 次に、コレクション(アンダースコアなど)を操作するためのライブラリーがトランスデューサーを作成できるクールなシナリオを実現でき、コレクションだけではない他のライブラリー(FRPなど)は単にトランスデューサーをサポートします。



Richが一見して提供する仕様は、Reducedの詳細を除き、JavaScriptにかなり適しています。 実際、ClojureにはすでにグローバルなReducedがあります(長い間存在していました)が、JavaScriptにはありません。 もちろん、簡単に作成できますが、各ライブラリは独自のReducedを作成します。 その結果、たとえば、Kefir.jsでトランスデューサーのサポートを追加する場合、トランスデューサー-アンダースコア、トランスデューサー-LoDashなどのサポートを追加する必要があります。 削減は、リッチが提供する仕様の弱点です。



別のシナリオは、トランスデューサーに関するさまざまなライブラリーの出現であり、各ライブラリーには独自の仕様があります。 そうすれば、メリットの一部しか得られません。 もちろん、 transducers.jsライブラリは既にありますが、もちろん独自のReducedがあり、これまでは最終呼び出しと初期呼び出しはサポートされておらず、作成者がどの形式で追加するかは不明です。



さて、多くのトランスデューサーが新しくて非常に有用なものではないように思われるという事実を考えると、私たちがどのようになるか、またはそれらをJavaScriptで使用するかどうかはまだ明確ではありません。






All Articles