JSのトランスデューサー-本当に必要ですか?

関数型アプローチは、ほとんどすべての最新のプログラミング言語にゆっくりと簡単に入り込んでいます。 モナドのようないくつかの要素(「エンドファンクターのカテゴリのモノイド、問題は何ですか?」)は主流にとって非常に議論の的ですが、他の要素(マップ、リデュース、フィルター変換など)は事実上の標準になっています。



画像






すべての利点を備えた、聖三位一体マップ/フィルター/リデュース-JSでは、メモリーに対して非常に経済的に機能しません。 壮大な建築松葉杖- トランスデューサー -は、ClojureからJSへの移植に成功し、理解しにくいことで初心者を攻撃しますが、過剰なメモリ割り当ての問題を解決するようです。



Transducers.js (トランスデューサーのプロトコル)のドキュメントを読んだとき、私はdeja vuの強い感覚を残しませんでした-どこかに似たようなものを見ました。 ええ-MDNのイテレーターとジェネレーター ! すべての健全なブラウザーとサーバーランタイムは、それを行う方法を既に知っています(イシャークについては黙ってください!)。



少し前まで、私はこれらのことを実験して、中間配列を作成せずにJSでデータをストリーミングできるものを作成しました。



それでは、行きましょう。



スタイリッシュでファッショナブルな若者にするには、インターネットの2つの機能が先駆けです。



const compose = (...fns) => x => fns.reduceRight((c, f) => f(c), x)
      
      





広範なコメントは不要です-関数の古典的な構成:compose(f、g、k)はf(g(k(x)))です。 ええ、私は括弧のようなものではありませんでした。



2番目(ここでは機能主義について覚えていますか?):



 const curry = (fn, a = []) => (...args) => a.length + args.length >= fn.length ? fn(...a, ...args) : curry(fn, [...a, ...args]);
      
      





引数の束を持つ関数を1つの引数の関数に変換します。 条件付きf(a、b)の場合、カリー(f)を呼び出すと、関数g-g(a、​​b)またはg(a)(b)のいずれかと呼ばれるfのラッパーが返されます。 主なことは、ラップされる関数が安定した数の引数を持っていることです(デフォルト値を持つ引数はありません)。



次に、ジェネレーターを使用してマップ関数を再発明します。



 function *_gmap(f, a) { for (let i of a) yield f(i); } const gmap = curry(_gmap);
      
      





コードは基本的なもので、関数f(a)を使用して入力aを調べ、答えを出します。 gmap(f、a)gmap(f)(a)の両方を呼び出すことができます。 これにより、部分的に適用されたgmap(f)を変数に保存し、必要に応じて再利用できます。



今フィルタリング:



 function *_gfilter(p, a) { for (let i of a) if (p(i)) yield i; } const gfilter = curry(_gfilter);
      
      





すぐに呼び出すこともできます-gfilter(f、a) 、そして機能主義の最高の伝統-gfilter(f)(a)



簡単にするために、さらにいくつかのプリミティブ関数(Lispに触発された):



 function *ghead(a) { for (let i of a) { yield i; break; } } function *gtail(a) { let flag = false; for (let i of a) { if (flag) { yield i; } else { flag = true; } } }
      
      





ghead(a)は、入力から最初の要素、 gtail(a) -最初を除くすべてを返します。



さて、これをすべて使用する方法の小さな例:



 let values = [3, 4, 5, 6, 7, 8, 9]; const square = x => x * x; const squareNfirst = compose(ghead, gmap(square)); let x = [...squareNfirst(values)];
      
      





1つの要素の配列は変数xに分類されます。



 const moreThan5 = gfilter(x => x > 5); let xxx = [...moreThan5(values)];
      
      





一般的な考え方は、gmapとgfilterに配列または反復可能なプロトコルを実装する何かを与えることができるということです-また、出力にはイテレータがあり、ES6では3つのポイント( let x = [... squareNfirst(values) ] )。



しかし、何を減らすか、あなたは尋ねるかもしれませんか? ここでは普遍的なアプローチはありません。または、古典的な[] .reduce(f、init)を使用するか、次のようにします。



 function _greduce(f, i, a) { let c = i; for(let v of a) c = f(c, v); return c; } const greduce = curry(_greduce);
      
      





greduce(f、i、a)は、着信配列または反復子を1つの値に折りたたみます。



例:



 const mixFn = compose(greduce((c, v) => c + v, 0), square, moreThan5); let yyy = mixFn(values);
      
      





複合関数は、入力から5より大きい数を順番に切り取り、結果の要素を二乗し、最後にreduceで合計します。



なんでこんなに大騒ぎするの?



イテレータでの処理はメモリ消費が少ないという事実からの主な利益。 一連の変換により、1つの要素を正確にドラッグします。 さらに、変換のチェーンにghead(a)のような関数がある場合、「遅延」が発生します。 そのghead()は取得しません-計算すらされません。



さて、機能的で、今ではファッショナブルです:)



このアプローチが、数十メガバイトのアレイを処理する際に少しのメモリを節約するのに役立つことを願っています。 そうでなければ、サイクルさえしないでください。



All Articles