純粋なJavaScriptトランスデューサーパス

いわゆる「トランスデューサー」について聞いたことがありますが、それでもJavaScriptの開発に使用しない場合は、今日、「トランスデューサーとは」と「使用方法」という質問に対する答えを見つける機会があります。 これにより、プロジェクトでそれらが必要かどうかを理解できるようになり、必要に応じてそれらの使用を開始するのに役立ちます。







それは、あまり多くのメモリを消費せずにパイプラインをリンクするのに適したデータ変換を構築するように設計されたコードをどのように書くかについてです。 トランスデューサの概念を適切に理解するために、データを折り畳むための単純なメカニズム、減速機、または機能から始めます。



減速機



レデューサーは、ストレージオブジェクトと特定のオブジェクト要素を入力として受け入れ、この要素をドライブに配置する機能です。 たとえば、レデューサーは次のとおりです。



(acc, val) => acc.concat([val])



。 転送されたドライブが配列[1, 2, 3]



で、要素が数字4



場合、配列[1, 2, 3, 4]



を返します。



 const acc = [1, 2, 3]; const val = 4; const reducer = (acc, val) => acc.concat([val]); reducer(acc, val) ///=> 1, 2, 3, 4
      
      





この場合、リデューサーは、渡された要素のリストとユニット要素を連結した結果を返します。



同様の別のレデューサーがあります: (acc, val) => acc.add(val)



。 このオブジェクトを返す.add()



メソッドを持つオブジェクトに適しています( Set.prototype.add()など )。 レデューサーは、メソッドを使用して、渡された要素をドライブに追加します。 add()



ドライブをadd()



ます。



 const acc = new Set([1, 2, 3]); const val = 4; const reducer = (acc, val) => acc.add(val); reducer(acc, val) ///=> Set{1, 2, 3, 4}
      
      





連結レデューサーを使用して反復可能なオブジェクトから配列を作成する関数を次に示します。



 const toArray = iterable => { const reducer = (acc, val) => acc.concat([val]); const seed = []; let accumulation = seed; for (value of iterable) {   accumulation = reducer(accumulation, value); } return accumulation; } toArray([1, 2, 3]) //=> [1, 2, 3]
      
      





普遍的な還元関数を取得することにより、 reducer



変数とseed



変数を新しい関数のパラメーターにすることができます(考慮されたばかりの引数との類似により、引数iterable



受け入れます)。



 const reduce = (iterable, reducer, seed) => { let accumulation = seed; for (const value of iterable) {   accumulation = reducer(accumulation, value); return accumulation; } reduce([1, 2, 3], (acc, val) => acc.concat([val]), []) //=> [1, 2, 3]
      
      





JavaScriptは、最初のパラメーターがリデューサーであるreduce



ような関数の記述に関する合意に向かって進化しています。 この関数をJavaScriptAllongéスタイルで書き換えると、次のようになります。



 const reduceWith = (reducer, seed, iterable) => { let accumulation = seed; for (const value of iterable) {   accumulation = reducer(accumulation, value); } return accumulation; } reduce([1, 2, 3], (acc, val) => acc.concat([val]), []) //=> [1, 2, 3] //    : reduceWith((acc, val) => acc.concat([val]), [], [1, 2, 3]) //=> [1, 2, 3]
      
      





JavaScriptの配列には、組み込みの.reduce



メソッドがあります。 このメソッドは、上記のreduce



およびreduceWith



関数とまったく同じように動作します。



 [1, 2, 3].reduce((acc, val) => acc.concat([val]), []) //=> [1, 2, 3]
      
      





これで、関数(acc, val) => acc.concat([val])



は不要なメモリ負荷を作成するため、次のようなリデューサーに置き換えることができます。 (acc, val) => { acc.push(val); return acc; }



(acc, val) => { acc.push(val); return acc; }



(acc, val) => { acc.push(val); return acc; }







形式(acc, val) => (acc.push(val), acc)



レコードは、セマンティックの観点からはより良く見えますが、コンマ演算子は、その使用の機能に慣れていない人を混乱させる可能性があることに注意してください。 これは通常、実稼働コードでは避けるのが最善です。



いずれの場合でも、要素を配列に収集するレデューサーを取得します。 名前を付けて、 reduceWith



関数を渡そうとします。



 const arrayOf = (acc, val) => { acc.push(val); return acc; }; reduceWith(arrayOf, [], [1, 2, 3]) //=> [1, 2, 3]
      
      





別の減速機があります。



 const sumOf = (acc, val) => acc + val; reduceWith(sumOf, 0, [1, 2, 3]) //=> 6
      
      





あるタイプの反復可能なオブジェクト(配列など)を別のタイプのオブジェクト(数値など)に縮小するレデューサーを作成できます。



減速機を飾る



JavaScriptを使用すると、他の関数を返す関数を簡単に作成できます。 たとえば、これはレデューサーを作成できる関数です。



 const joinedWith = separator =>   (acc, val) =>     acc == '' ? val : `${acc}${separator}${val}`; reduceWith(joinedWith(', '), '', [1, 2, 3]) //=> "1, 2, 3" reduceWith(joinedWith('.'), '', [1, 2, 3]) //=> "1.2.3"
      
      





さらに、JSでは、他の関数を引数として取る関数を作成することは完全に自然です。



デコレータは、関数を引数として取り、その引数に意味的に関連付けられている別の関数を返す関数です。 たとえば、この関数は、関数型プログラミングの言語を話す場合、バイナリの2つの引数を持つ関数を取り、2番目の引数に1を追加して装飾します。



 const incrementSecondArgument = binaryFn =>   (x, y) => binaryFn(x, y + 1); const power = (base, exponent) => base ** exponent; const higherPower = incrementSecondArgument(power); power(2, 3) //=> 8 higherPower(2, 3) //=> 16
      
      





この例では、 higherPower



関数はexponent



単位を追加することで修飾されたexponent



です。 したがって、 higherPower(2,3)



を呼び出すと、 power(2,4)



と同じ結果が得られます。 私たちはすでに同様の関数を使用していますが、レデューサーもバイナリ関数です。 彼らは装飾することができます。



 reduceWith(incrementSecondArgument(arrayOf), [], [1, 2, 3]) //=> [2, 3, 4] const incremented = iterable =>   reduceWith(incrementSecondArgument(arrayOf), [], iterable); incremented([1, 2, 3]) //=> [2, 3, 4]
      
      





マッピング関数



マッピング用の関数を作成しました。この関数は、反復可能なオブジェクトを取得し、それぞれの値を1つずつ増やして値を処理した結果を返します。 JSでプログラムを開発するとき、私たちは常にマッピングに頼りますが、もちろん、このメカニズムを実装する関数は通常、要素が1増加する数値配列のコピーを作成するよりもわずかに大きいと予想されます。 incrementSecondArgument



関数をもう一度見てみましょう。



 const incrementSecondArgument = binaryFn =>   (x, y) => binaryFn(x, y + 1);
      
      





減速機を装飾するために使用するため、より適切な名前を付けます。



 const incrementValue = reducer => (acc, val) => reducer(acc, val + 1);
      
      





これで、コードを読み取るときに、 incrementValue



がレデューサーを引数として受け取り、渡された要素を処理する前にレデューサーを追加する別のレデューサーを返すことがすぐにわかりincrementValue



。 「増分」のロジックをパラメーターに入れることができます。



 const map = fn =>   reducer =>     (acc, val) => reducer(acc, fn(val)); const incrementValue = map(x => x + 1); reduceWith(incrementValue(arrayOf), [], [1, 2, 3]) //=> [2, 3, 4]
      
      





これはすべて、関数を引数として受け取り、関数を引数として受け取る他の関数を返す関数に慣れていない人には珍しいように見えますが、 map(x => x + 1)



構造をどこにでも配置できます。ここでincrementValue



を使用incrementValue



。 したがって、次のように書くことができます。



 reduceWith(map(x => x + 1)(arrayOf), [], [1, 2, 3]) //=> [2, 3, 4]
      
      





また、 map



デコレータは任意のレジューサーをデコレートできるため、数値をインクリメントした結果を組み合わせて文字列を形成したり、合計したりすることができます。



 reduceWith(map(x => x + 1)(joinedWith('.')), '', [1, 2, 3]) //=> "2.3.4" reduceWith(map(x => x + 1)(sumOf), 0, [1, 2, 3]) //=> 9
      
      





上記の手法を使用して、1から10までの数の平方和を見つけてください。



 const squares = map(x => power(x, 2)); const one2ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; reduceWith(squares(sumOf), 0, one2ten) //=> 385
      
      





ご覧のとおり、成功しました。 次に進みましょう-フィルタについて話しましょう。



フィルター



最初の減速機に戻ります。



 const arrayOf = (acc, val) => { acc.push(val); return acc; }; reduceWith(arrayOf, 0, one2ten) //=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      
      





5を超える数のみを取得する場合はどうなりますか? 簡単に。



 const bigUns = (acc, val) => { if (val > 5 ) {   acc.push(val); } return acc; }; reduceWith(bigUns, [], one2ten) //=> [6, 7, 8, 9, 10]
      
      





当然のことながら、5乗を超える数の配列を取得するために、すでにわかっているすべてのものを組み合わせることができます。



 reduceWith(squares(bigUns), [], one2ten) //=> [9, 16, 25, 36, 49, 64, 81, 100]
      
      





しかし、ここで判明したのは私たちが必要とするものではありません。 出力は、2乗が5を超える数値ではなく、2乗が5を超える数値です。 番号は、番号ではなく、平方する前に選択しなければなりません。 システムのこの動作を実現することはそれほど難しくありません。 ここでの一番下の行は、数値のフィルタリングを担当するデコレーターが私たちを助けてくれるので、それを使ってレデューサーを飾ることができます。



 reduceWith(squares(arrayOf), [], one2ten) //=> [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] const bigUnsOf = reducer =>   (acc, val) =>     (val > 5) ? reducer(acc, val) : acc; reduceWith(bigUnsOf(squares(arrayOf)), [], one2ten) //=> [36, 49, 64, 81, 100]
      
      





bigUnsOf



関数bigUnsOf



非常に具体的です。 ここでは、 map



と同じことを行いmap



。つまり、述語関数を抽出し、それを引数にします。



 reduceWith(squares(arrayOf), [], one2ten) //=> [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] const filter = fn =>   reducer =>     (acc, val) =>       fn(val) ? reducer(acc, val) : acc; reduceWith(filter(x => x > 5)(squares(arrayOf)), [], one2ten) //=> [36, 49, 64, 81, 100]
      
      





もちろん、フィルターはどれでもかまいません。 それらに名前を付けて繰り返し使用したり、匿名関数を使用したりできます。



 reduceWith(filter(x => x % 2 === 1)(arrayOf), [], one2ten) //=> [1, 3, 5, 7, 9]
      
      





フィルターを使用して、1から10までの奇数の平方和を見つけます。



 reduceWith(filter(x => x % 2 === 1)(squares(sumOf)), 0, one2ten) //=> 165
      
      





トランスフォーマーと構成



「トランスフォーマー」という用語は、他のプログラミング言語からJavaScriptに来ました。 これは、引数を取り、それを別のものに変換する関数の名前です。 上記の「デコレータ」と呼ばれるのは、トランスの特殊なケースです。 したがって、あるリデューサーを別のリデューサーとする変換関数に関する話をどこかで会った場合、リデューサーを「装飾」する同じ機能について話していることは明らかであり、マッピングやフィルタリングなどの追加機能を追加します。



先ほど説明したマッピング関数とフィルターもトランスフォーマーです。 このプログラミングパターンのコンテキストでは、トランスフォーマーの最も重要な特性は、構成を使用して新しいトランスフォーマーを作成することです。 ここでは、より明確にするために、入力時に送信された任意の2つの関数の構成を提供する関数。



 const plusFive = x => x + 5; const divideByTwo = x => x / 2; plusFive(3) //=> 8 divideByTow(8) //=> 4 const compose2 = (a, b) =>   (...c) =>     a(b(...c)); const plusFiveDividedByTwo = compose2(divideByTwo, plusFive); plusFiveDividedByTwo(3) //=> 4
      
      





トランスフォーマーは構成を使用して新しいトランスフォーマーを作成します。 compose2



に適用した場合、これはどういう意味ですか? これは、彼女に任意の2つのトランスフォーマーを与えると、減速機を変換する新しいトランスフォーマーを取得することを意味します。 したがって、以下が得られます。



 const squaresOfTheOddNumbers = compose2( filter(x => x % 2 === 1), squares ); reduceWith(squaresOfTheOddNumbers(sumOf), 0, one2ten) //=> 165
      
      





squaresOfTheOddNumbers



という名前で隠されているのは、 compose2



関数をフィルターとマッピング関数に適用することで作成したトランスフォーマーです。



デコレータを作成する機会ができたので、高度な接続性によって区別される複雑なコードを、非常に特殊な小さなブロックに分解します。



トランス構成



compose2



関数がどのように機能するかを知っているcompose2



、2つの関数の合成を取得できます。任意の数の関数の合成が必要な場合の対処方法を考えます。 答えは畳み込みです。



compose2



を書き直し、 compositionOf



トランスフォーマーを作成します。



 const compositionOf = (acc, val) => (...args) => val(acc(...args));
      
      





これで、引数の縮小として任意の数の関数の合成を取得するために、合成関数をcompose



できます。



 const compose = (...fns) => reduceWith(compositionOf, x => x, fns);
      
      





だから、私たちは最も興味深いことになります。



変換器



次のエントリを検討してください。



 reduceWith(squaresOfTheOddNumbers(sumOf), 0, one2ten)
      
      





ここでは4つの要素を区別できます。 減速機用のトランスフォーマー(トランスフォーマーの構成)、初期値(ドライブ)および反復可能オブジェクト。 トランスフォーマー、レデューサー、ドライブ、反復可能なオブジェクトを別々のパラメーターに入れると、次のようになります。



 const transduce = (transformer, reducer, seed, iterable) => { const transformedReducer = transformer(reducer); let accumulation = seed; for (const value of iterable) {   accumulation = transformedReducer(accumulation, value); } return accumulation; } transduce(squaresOfTheOddNumbers, sumOf, 0, one2ten) //=> 165
      
      





いくつかのプログラミング言語では、変数またはパラメーターの長い名前を減らしたいという強い要望があることに注意してください。 その結果、かなり長い名前transformer



xform



またはxf



短縮されます。 レコードが(xf、reduce、seed、col)、またはxf((val, acc) => acc) -> (val, acc) => acc



ように見える類似の構造を見て驚かないでください。 ここでは省略せずに実行できますが、生産コードでxform



xf



xform



ような名前xform



まったく受け入れられます。



そして今、実際には、それがすべて書かれていたことのために。 レデューサーは、 .reduce —



などのメソッドに渡される関数です。ストレージオブジェクトと入力データを受け取り、新しいデータが配置されるドライブを返します。 トランスフォーマーは、減速機を別の減速機に変換する機能です。 そして、トランスデューサー(この名前は「トランスフォーマー」と「リデューサー」という用語を組み合わせた結果です。ここでは、「レデューサーとは何ですか?」という質問に対する答えです)特定の値で。



変換器テンプレートの優雅さは、トランスの構成が自然に新しいトランスの作成につながるという事実にあります。 その結果、必要な数のトランスフォーマーをチェーンできます。 結果は1つの変換されたレデューサーであり、反復コレクションを1回だけ通過する必要があるため、これは非常に重要です。 データの中間コピーを作成したり、それらに対して複数のパスを実行する必要はありません。



トランスデューサーは、Clojure言語からJavaScriptにアクセスしましたが、ご覧のとおり、JavaScriptに完全に適合しているため、実装には言語の標準機能で十分です。



だから、誰かがトランスデューサーとは何かを私たちに尋ねたら、次のように答えることができます:



注:このフラグメントは強調表示する必要があります。



変換器は合成に適した変換器で、反復可能なオブジェクトを折り畳むための減速器に適用されます。



動作中のトランスデューサー



上記では、トランスデューサーの構築につながるコード断片を調べました。 JSエディターで既にそれらを再現してテストしている可能性は十分にありますが、ここでは、便宜上、わかりやすくするために、すべてのコードを1か所に集めています。



 const arrayOf = (acc, val) => { acc.push(val); return acc; }; const sumOf = (acc, val) => acc + val; const setOf = (acc, val) => acc.add(val); const map = fn =>   reducer =>     (acc, val) => reducer(acc, fn(val)); const filter = fn =>   reducer =>     (acc, val) =>       fn(val) ? reducer(acc, val) : acc; const compose = (...fns) => fns.reduce((acc, val) => (...args) => val(acc(...args)), x => x); const transduce = (transformer, reducer, seed, iterable) => { const transformedReducer = transformer(reducer); let accumulation = seed; for (const value of iterable) {   accumulation = transformedReducer(accumulation, value); } return accumulation; }
      
      





この例は、配列を操作するときに通常使用されるすべてのもの、つまり.map



.filter



.reduce



。すぐに、処理済みデータセットの複数のコピーの作成に関与しない構成に適したトランスデューサーがあります。 実際、実際のプロジェクト用に作成されたトランスデューサーは、 .find



メソッドの機能を再現するなど、はるかに多くのユース.find



提供します。



transduse



関数transduse



反復可能なコレクションに転送されるように設計されていることに注意してください。さらに、初期値(ドライブ)とリデューサーを提供する必要があります。 ほとんどの場合、初期値とリデューサーは、同じタイプのすべてのコレクションで同じ関数です。 これは、各ライブラリにも当てはまります。



オブジェクト指向プログラミングでは、この問題はもちろん、ポリモーフィズムによって解決されます。 コレクションにはメソッドがあるため、適切なメソッドを呼び出すことで、必要なものを出力として取得します。 量産コードの作成に使用されるライブラリは、さまざまなタイプのコレクションのインターフェイスを提供し、トランスデューサーの使用を便利にします。



トランスデューサの基礎となるテンプレートを理解し、この言語が第一級の機能を提供する便利で便利な機能を評価するには、上記で十分であると考えています。



変換器:ユーザーリスト処理



この資料では、次の問題を解決するためのオプションを示します。ユーザーとユーザーが訪れる場所のセットがあり、どちらもハッシュコードのリストの形式で表示されます。 各行の最初のコードはユーザー、2番目のコードは訪問した場所、たとえばレストランや店です。 リスト内のデータの順序が重要です。 課題は、場所間のどの遷移が最も人気があるかを見つけることです。



つまり、次のようなリストがあります。



 1a2ddc2, 5f2b932 f1a543f, 5890595 3abe124, bd11537 f1a543f, 5f2b932 f1a543f, bd11537 f1a543f, 5890595 1a2ddc2, bd11537 1a2ddc2, 5890595 3abe124, 5f2b932 f1a543f, 5f2b932 f1a543f, bd11537 f1a543f, 5890595 1a2ddc2, 5f2b932 1a2ddc2, bd11537 1a2ddc2, 5890595 ...
      
      





このリストを注意深く見ると、ユーザー1a2ddc2



がコード5890595



5f2b932



bd11537



、および5890595



ます。 同時に、ユーザーf1a543f



は、場所5890595



5f2b932



bd11537



、および5890595



。 などなど。



人々が通常どこへ行くのかを知る必要があると仮定すると、「場所A」から「場所B」への最も人気のある遷移を見つける必要があります。 1a2ddc2



の移動履歴は、 5f2b932



bd11537



5890595



です。 これは、彼にとって、そのような移行スキームを場所から場所へと構築できることを意味します。



 5f2b932 -> bd11537 bd11537 -> 5890595 5890595 -> 5f2b932 5f2b932 -> bd11537 bd11537 -> 5890595
      
      





各ユーザーに対して同様のリストを作成する必要があることに注意してください。 これが完了したら、最も人気のあるトランジションを見つける必要があります。 同様のカウントは次のようになります。



遷移5f2b932 -> bd11537



がリストに2回表示されます。

遷移bd11537 -> 5890595



も2回発生します。

遷移5890595 -> 5f2b932



は一度だけ会った。



今やるべきことは、すべてのユーザーのクリック数を計算し、最も人気のあるものを見つけることです。 トランスデューサーを使用したこの問題の解決策を次に示します。



 const logContents = `1a2ddc2, 5f2b932 f1a543f, 5890595 3abe124, bd11537 f1a543f, 5f2b932 f1a543f, bd11537 f1a543f, 5890595 1a2ddc2, bd11537 1a2ddc2, 5890595 3abe124, 5f2b932 f1a543f, 5f2b932 f1a543f, bd11537 f1a543f, 5890595 1a2ddc2, 5f2b932 1a2ddc2, bd11537 1a2ddc2, 5890595`;
      
      





 const asStream = function * (iterable) { yield * iterable; }; const lines = str => str.split('\n'); const streamOfLines = asStream(lines(logContents)); const datums = str => str.split(', '); const datumize = map(datums); const userKey = ([user, _]) => user; const pairMaker = () => { let wip = []; return reducer =>   (acc, val) => {     wip.push(val);     if (wip.length === 2) {       const pair = wip;       wip = wip.slice(1);       return reducer(acc, pair);     } else {       return acc;     } } } const sortedTransformation = (xfMaker, keyFn) => {   const decoratedReducersByKey = new Map();   return reducer =>     (acc, val) => {       const key = keyFn(val);       let decoratedReducer;       if (decoratedReducersByKey.has(key)) {         decoratedReducer = decoratedReducersByKey.get(key);       } else {         decoratedReducer = xfMaker()(reducer);         decoratedReducersByKey.set(key, decoratedReducer);       }       return decoratedReducer(acc, val);     } } const userTransitions = sortedTransformation(pairMaker, userKey); const justLocations = map(([[u1, l1], [u2, l2]]) => [l1, l2]); const stringify = map(transition => transition.join(' -> ')); const transitionKeys = compose( stringify, justLocations, userTransitions, datumize ); const countsOf = (acc, val) => {   if (acc.has(val)) {     acc.set(val, 1 + acc.get(val));   } else {     acc.set(val, 1);   }   return acc; } const greatestValue = inMap => Array.from(inMap.entries()).reduce(   ([wasKeys, wasCount], [transitionKey, count]) => {     if (count < wasCount) {       return [wasKeys, wasCount];     } else if (count > wasCount) {       return [new Set([transitionKey]), count];     } else {       wasKeys.add(transitionKey);       return [wasKeys, wasCount];     }   }   , [new Set(), 0] ); greatestValue( transduce(transitionKeys, countsOf, new Map(), streamOfLines) ) //=>   [     "5f2b932 -> bd11537",     "bd11537 -> 5890595"   ],   4
      
      





まとめ



この資料が、誰もがトランスデューサーを永久的なツールにするのに役立つことを願っています。 彼らの研究を掘り下げたい場合は、ここにトランスデューサーに関する優れた資料があり、GitHubのtransdusers-jsライブラリーがあります。



親愛なる読者! JavaScriptプロジェクトでトランスデューサーを使用していますか?



All Articles