Clojure-トランスデューサー、減速機、その他のかす

最近、 トランスデューサーは名声を得ました-Clojure 1.7の新機能で、まだリリースされていません。 執筆時点では、Clojure 1.7-alpha5が関連していますが、 PythonRubyJavaScriptPHPJavaC ++LuaErlangなどのさまざまな言語でかなりの数のトランスデューサーポートが既に登場しています。 そして...それは少しがっかりです。 結局、かなり前に(Clojure 1.5に戻って)、 reducersライブラリが追加されました 。だから、 リデューサーについては誰も話さず、何も移植しませんでした。



Clojureでこれらすべての減速機とトランスデューサーが必要だった理由を見てみましょう(本当に必要なのでしょうか?)、それらがどのように機能し、どのように使用するのか...



この言語のコンテキスト外でClojureに由来する概念を記述するのは間違っています。 したがって、Clojureには多くのリストがあります。 しかし、マタンは​​ありません。 一般に、Clojureの初期知識(特にシーケンスの概念)は適切ですが、Haskellを知る必要はありません。 また、リストされている標準関数のリストはすべて実際には非常に変更されており、時には「わずかに」壊れていることも事前に警告します。 簡素化のためにすべて。 そうそう、写真は同じブリトーです。



オフにします...



そのため、Clojureは関数型言語です。つまり、 通常の 命令型 サイクルは良くありません。

さて、大丈夫、私たちは本当に望んでいませんでした-機能的に楽しい還元があります!



(defn my-reduce ([rf coll] ;;     (if-let [s (seq coll)] (my-reduce rf (first s) (next s)) (rf))) ([rf acc coll] (if-let [[x & xs] (seq coll)] (recur rf (rf acc x) xs) acc)))
      
      





実際には、 reduce



はもちろん、 少し異なる方法で実装されますが、今は重要ではありません。忘れてはいけません。 rf



関数(reduct関数と呼びましょう)は、ここで2つの引数を取ります。最初の引数は、一種の「移動状態」です。 2番目はcoll



シーケンスの要素です。 初期状態が指定されていない場合、 (first coll)



または(rf)



ます。 状態acc



を「ドラッグ」しながら、 rf



と呼ぶ各要素について、コレクションcoll



全体をacc



ます。 要素が完成したら、 acc



返します。



小さな例。 文字列のリストがあり、それらの合計の長さを計算するとします。

ループを使用した命令コードは次のとおりです。



 (defn length-of-strings [strings] (with-local-vars [acc 0] ;; -,  Clojure   ! (doseq [c strings] (var-set acc (+ @acc (count c)))) ;;    @acc))
      
      





ループ状態は、単純なacc



(数値)カウンターです。 各反復で、 (+ @acc (count c))



に等しく設定します。

そしてもう1回、 reduce



のみをreduce



ます。



 (defn length-of-strings [coll] (my-reduce (fn ([acc c] (+ acc (count c)))) ;;  - 0 ;;   coll))
      
      





一時的に怠lazを「忘れる」場合、 map



filter



などの多くのプリミティブな操作を実装できfilter







 (defn my-map [f coll] (my-reduce (fn [acc c] (conj acc (fc))) [] coll)) (defn my-filter [p coll] (my-reduce (fn [acc c] (if (pc) (conj acc c) acc)) [] coll))
      
      





take



を実装take



指定reduce



れたreduce



オプションは機能しなくなります-サイクルは常にシーケンス全体を実行します(これは、すべてが遅延しているHaskellの一種ではありません)。



この欠点を克服するために、バージョン1.5では特別な松葉杖マーカーを追加し、対応する述語をreduced?



。 同時に、 reduce



を書き直し、次のようなものを得ました:



 (defn my-reduce ([rf coll] (if-let [s (seq coll)] (my-reduce rf (first s) (next s)) (rf))) ([rf acc coll] (if-let [[x & xs] (seq coll)] (let [ret (rf acc x)] (if (reduced? ret) @ret (recur rf ret xs))) acc)))
      
      





reduct関数が返されると(reduced ...)



、ループが中断し、値@ret



ます。



 (defn take-r [n coll] (my-reduce (fn [[n1 acc] c] (if (pos? n1) [(dec n1) (conj acc c)] (reduced acc))) [n []] coll)) ;;    ! (take-r 5 (range)) ;; => [0 1 2 3 4]
      
      





素晴らしい削減機能を思い出さずにはいられません。 本質的に、これはreduce



類似物であり、最後だけでなくacc



すべての中間値の遅延リストのみを返します。 デバッグ時に使用すると非常に便利です。 アルゴリズムステップを関数として記述し、入力データをreduce



してコレクションに対してreduce



を実行します。 何かが間違っている場合は、 reduce



reductions



に置き換え、REPLで実行し、すべての中間ステップを取得します。 サイクルでは、それほど簡単ではありません-デバッグ松葉杖を修正する必要がありますが、これはあまり便利ではありません。



削減はそれ自体で有用である可能性があります;階乗は計算するためにあります:



 ;; => **   (def factorials (reductions *' (cons 1 (map inc (range))))) (nth factorials 20) ;; => 2432902008176640000
      
      





Clojureはシーケンスを使用してコレクションを循環します。 ベクトル、ハッシュテーブル、または単純なイテレータを調べる場合、大量の一時オブジェクトがヒープ内に作成されます。



このような状況で要求される明らかな最適化は、これが理にかなっているコレクションに対して特別なreduce



オプションを実装することです。 コレクションがそのような最適化に役立たない場合は、記事の冒頭で示したものと同様の標準実装を使用してください。 これを行うには、特別なプロトコルclojure.core.protocol / CollReduceがあります。 コレクションオブジェクトがサポートする場合、この実装はclojure.core/reduce



内で使用されます。 したがって、Clojureのreduce



は通常、同様のdoseq



ループよりも高速です。



変圧器



トランスフォーマーは、1つのリダクト関数を取り、新しいリダクト関数を返す関数です。

たとえば、「1ずつ増加する」トランスフォーマーを次に示します。



 (defn inc-t [rf] (fn [acc c] (rf acc (inc c)))) ;;     (reduce + 0 (map inc [1 2 3 4])) ;; => 14 (reduce (inc-t +) 0 [1 2 3 4]) ;; => 14
      
      





この問題を多少一般化して、 inc



代わりに関数を指定することができます。



 (defn map-t [f] (fn [rf] (fn [acc c] (rf acc (fc))))) (def inc-t (map-t inc)) (def dec-t (map-t dec)) ;; ... (reduce (inc-t +) 0 [1 2 3 4]) ;; => 14
      
      





そして、ここで、例えば、トランスフォーマー「フィルター」:



 (defn filter-t [pred] (fn [rf] (fn [acc c] (if (pred c) (rf acc c) acc)))) (def odd?-t (filter-t odd?)) (def even?-t (filter-t even?)) ;;  (reduce (even?-t *) 1 [1 2 3 4]) ;; => 8
      
      





複数の変圧器を組み合わせることは可能ですか? もちろん!



 (defn odd?-inc-t [rf] (odd?-t (inc-t rf))) ;; ..    (def odd?-inc-t (comp (filter-t odd?) (map-t inc))) ;;   .. (def odd?-inc-t (comp (fn [rf] (fn [acc c] (if (odd? c) (rf acc c) acc))) (fn [rf] (fn [acc c] (rf acc (inc c)))))) ;;      (defn odd?-inc-t [rf] (fn [acc c] (if (odd? c) (rf acc (inc c)) acc))) ;;   (reduce * 1 (->> [1 2 3 4 5] (filter odd?) (map inc))) ;; => 48 (reduce (odd?-inc-t *) 1 [1 2 3 4 5]) ;; ==> 48
      
      





トランスフォーマーが「逆」の順序で進むことに注意する価値があります。 コレクションの要素をB



に到達する前にトランスフォーマA



で処理したい場合、 (comp AB)



ようにそれらを接着する必要があります。 そして今、トリック:



 (def cc (vec (range 1000000))) (time (reduce + 0 (->> cc (filter odd?) (map inc)))) ;; "Elapsed time: 171.390143 msecs" ;; => 250000500000 (time (reduce ((comp (filter-t odd?) (map-t inc)) +) 0 cc)) ;; "Elapsed time: 93.246015 msecs" ;; => 250000500000
      
      





方法は次のとおりです、明白な速度の急激な増加! もちろん、すべては多くの詳細とさまざまなニュアンスに依存するため、実際にはゲインは異なる場合があります。 一般に、このコードをベンチマークとして使用するべきではないと言いたいです。



しかし、全体として、結果は驚くにはほど遠いです。 map



filter



を使用map



と、2つの中間シーケンスが作成されます。 元のベクトルを実行し、フィルター処理された値の一時的なリストを作成します。 次に、このリストを調べて、要素を拡大した別のリストを作成します。 そして最後に、値をまとめて、すでに説明します。



一方、トランスフォーマーを使用したオプションでは、一時的なコレクションは作成されません。 代わりに、 odd?



ソース要素にすぐに適用されodd?



、およびinc







私の減速機はどこにありますか?



そして、バージョン1.5が新しい標準ライブラリclojure.core.reducers



導入するまで、すべては順調clojure.core.reducers



。 そうです、 別のライブラリを明示的にインポートする必要があります。 また、 map



filter



take-while



などのバージョンも発表しました。 そして、もちろん、それらはclojure.core



通常のバージョンと互換性がありません。 したがって、単純な(use 'clojure.core.reducers)



代わりに(use 'clojure.core.reducers)



(require '[clojure.core.reducers :as r])



と記述する方が適切です。



では、減速機とは何ですか? 簡単に言うと、愚かなことです:レデューサーとは、削減できるオブジェクトのことです。 clojure.core.reducers



観点でのコレクションはすべてレデューサーです。 ハッシュテーブルはレデューサーです。 java.lang.String



レデューサー。 まあ、もちろん、 nil



。 定義を見てみましょう:



 (defn reducer [coll xf] ;; `xf` -   (reify clojure.core.protocols/CollReduce (coll-reduce [this f1] (let [f2 (xf f1)] (clojure.core.protocols/coll-reduce coll f2 (f2)))) (coll-reduce [this f1 init] (let [f2 (xf f1)] (clojure.core.protocols/coll-reduce coll f2 init)))))
      
      





ここでcoll



コレクションが取得され、新しいコレクションが返されます。そこで、 reduce



を実行できます。 要素を追加したり、削除したり、要素を調べたりすることもできません。 ただし、 reduce



開始する前reduce



reduct関数はxf



トランスフォーマーを通過します。



 (def nums [1 2 3 4 5]) (def nums+1 (reducer nums inc-t)) (reduce + 0 nums) ;; => 15 (reduce + 0 nums+1) ;; => 20
      
      





すでに述べたように、reducersライブラリは独自のオプションmap



filter



take-while



などを発表しています。 それらはすべて減速機を受け入れ、対応する変圧器が「取り付けられている」新しい減速機を返します。



これはclojure.core.reducers/map



ようにclojure.core.reducers/map



(もちろん、見た目は大きく異なります )。



 (def map-r [f coll] (reducer coll (map-t f)))
      
      





そして今、これらすべてがどのように使用できるかのいくつかの例:



 (require '[clojure.core.reducers :as r]) (def nums [1 2 3 4 5 6 7 8]) (type (map inc nums)) ;; => clojure.lang.LazySeq (reduce conj [] (map inc nums)) ;; => [2 3 4 5 6 7 8 9] (type (r/map inc nums)) ;; => clojure.core.reducers$folder$reify__1234 ;; -  sequence (reduce conj [] (r/map inc nums)) ;; => [2 3 4 5 6 7 8 9] ;;      (reduce conj [] (r/filter odd? nums)) ;; => [1 3 5 7] (reduce + 0 (->> nums (r/map inc) (r/map inc))) ;; => 52 ;; ~~ (+ 0 (inc (inc 1)) (inc (inc 2)) ...) (reduce + 0 (->> nums (r/filter odd?) (r/map inc))) ;; => 20 ;; ~~ (+ 0 (inc 1) (inc 3) ...)
      
      





平行



正直に言うと、いわゆる「リデューサー」は無駄です。 「フォルダ」の方が正しいでしょう。 実際、 CollReduce



プロトコル(レデューサーよりもずっと前に登場した)に加えて、別のより重要なCollFold



プロトコルがライブラリで宣言されています。



 (defprotocol CollFold (coll-fold [coll n combinef reducef]))
      
      





原則として、これは非常に似ており、還元関数のみが2つになり、わかりにくい引数n



も追加されました。 アイデア:一部のコレクションは複数のスレッドで実行できます。 簡単に説明すると、サイズを約n



要素のブロックに分割し、各ピースを#(reduce reducef (combinef) %)



を使用して折りたたんでから、結果リスト(ブロックごとに1つ)を再び#(reduce combinef %)



が、 #(reduce combinef %)



ます。



並行して折りたたむことができるレデューサーは、 フォルダーと呼ばれます

CollFold



プロトコルをサポートしているのは、ベクターとハッシュテーブルの2つの標準コレクションのみです。



 (def v (vec (range 10000000))) ;; ,  1  (time (reduce + v)) ;; "Elapsed time: 648.897616 msecs" ;; => 49999995000000 ;;    (time (r/coll-fold v 512 + +)) ;; "Elapsed time: 187.414147 msecs" ;; => 49999995000000
      
      





これが理にかなっているすべての標準CollFold



実装しCollFold



。 これは、たとえば、 r/map



r/filter



r/mapcat



r/flatten



です。 一方、 r/take



r/take-while



r/drop



は並列化をサポートしていません。 上記はr/map



実装でした。 彼女の更新されたバージョンは次のとおりです。



 (def map-r [f coll] ;;   `reducer`  `folder` (folder coll (map-t f)))
      
      





coll-fold



直接使用する必要はありません-日常のニーズに合わせた折りたたみラッパーがあります。 n



のデフォルト値(ブロックサイズ)を512に設定します。一般に、ヒントは明確です-レデューサーは、大規模なコレクション(> 1Kアイテム)を対象としています。 また、 coll-fold



直接使用しないで、 coll-fold



を呼び出します。



ああ、まだfoldcatがあります。 一種の高速化(マルチスレッドによる)オプション#(reduce conj [] %)



。 この関数Counted



Sequable



、およびCollFold



を実装するclojure.core.reducers.Catオブジェクトを返します



 (r/map inc [1 2 3]) ;; => #<reducers$folder$reify__..... ;;   ** ? ;; ..     `reduce`  (reduce conj [] (r/map inc [1 2 3])) ;; => [2 3 4] ;;    ... (def v (vec (range 1000000))) (time (count (reduce conj [] (r/map inc v)))) ;; "Elapsed time: 90.124397 msecs" ;; => 1000000 ;; -  ,    `foldcat` (time (count (r/foldcat (r/map inc v)))) ;; "Elapsed time: 25.054988 msecs" ;; => 1000000 (time (count (r/foldcat (r/map inc (r/foldcat (r/map inc v)))))) ;; "Elapsed time: 32.054988 msecs" ;; => 1000000 ;;  `foldcat`, ,  foldable (, ) (satisfies? r/CollFold (r/foldcat [])) ;; => true
      
      





彼らはシーンに突入し......



減速機とは異なり、トランスデューサーは独立したライブラリではありません。 clojure.core



モジュールに直接統合されるのは、むしろ概念(アイデアを読んでください)です。 バージョン1.7でこの歓迎を待っています(少し残っています)。



簡単に説明すると、トランスデューサーは同じ名前のトランスブランドですが、ブランドが変更された後にのみ名前が変更されます。 さて、ほとんど:これから、還元関数は0と2の引数だけでなく、1を取ることもできます。トランスデューサは、それぞれ0-1-2-ary還元関数から新しい0-1-2-ary還元関数への関数です。 。



 (def typical-transducer (fn [rf] (fn ([] ...) ;;    ([acc] ...) ;; ... ([acc c] ...))) ;; ,    ,    ;;   `map-t`,  33%   (defn map-t-improved [f] (fn [rf] (fn ([] (rf)) ;;   ([acc] (rf acc)) ;;   ([acc c] (rf acc (fc)))))) ;;  `c`  `(fc)`
      
      





初期要素が必要な場合は、前述のように0項還元関数を呼び出すことができます。 実際、2進バリアントは、削減自体に使用されます。 1項オプションは、ジョブ全体の最後で( reduce



完了時に)呼び出されます。 最後の後に新しい要素を「追加」する必要がある場合に必要です。



例:コレクションからの繰り返しをスキップする重複排除トランスデューサー:



 (defn my-dedupe [] (fn [rf] ;; , ! (let [prev (atom ::none)] (fn ;;   - ([] (rf)) ([acc] (rf acc)) ([acc c] (let [p @prev] (reset! prev c) (if (= pc) acc (rf acc c)))))))) (def rf ((my-dedupe) +)) (reduce rf 0 [1 1, 2 2, 3, 1 1]) ;; => 7 (reduce rf 0 [1 1, 2 2, 3, 1 1]) ;; => 6 ;; ... `rf`  ,    2 
      
      





微妙な点は、トランスデューサーが新しい還元関数を返すことです。 さらに、この還元関数は可変状態を持ち、実際には3つの異なることを実行できます(アリティごとに1つ)。 まあ、ある種のオブジェクト。 しかし同時に、トランスデューサー自体には状態がなく、一種の工場としてのみ機能します。



reduct関数の1進バリアントの使用例として、 partition-allが指定されています。 簡素化された実装:



 (defn partition-all-t [n] (fn [rf] (let [buffer (java.util.ArrayList. n)] ;; ! (fn ([] (rf)) ([acc] (if (.isEmpty buffer) ;;    -   (rf acc) ;; ... (let [v (vec (.toArray buffer)) ;;     acc' (rf acc v)] ;;    2- `rf` ;;       (rf acc')))) ([acc c] (.add buffer c) (if (= n (.size buffer)) ;;    - ""  (let [v (vec (.toArray buffer))] (.clear buffer) (rf acc v)) ;;  -    acc)))))) ;;  ,   (   ,  (conj) => []) (reduce ((partition-all-t 3) conj) (range 10)) ; >> ClassCastException java.lang.Long cannot be cast to clojure.lang.IPersistentCollection ;;  ... ;;  ,    []... (reduce ((partition-all-t 3) conj) [] (range 10)) ;; => [[0 1 2] [3 4 5] [6 7 8]] ;; ,  ...
      
      





うーん... 0項オプションも1項オプション((partition-all-t 3) conj)



も呼び出されませんでした-通常のreduce



は、これらすべての革新について何も知りません。 コレクションが空の場合にのみ0-aryオプションを呼び出し、1-aryオプションはまったく呼び出しません。



したがって、彼らは新しいtransduce



関数を作成しました。 ここでは、「廃止された」 reduce



とは異なり、初期状態が明示的に設定されていない限り、常に(rf)



呼び出します。 そして、この関数は(rf acc)



、そして一度だけ呼び出すことが保証されています。 そして、 transduce



自体が変換器を呼び出し、可変還元機能を目から隠します。 言い換えれば、すべての汚い作業(副作用の観点から)は「内部」で行われます。



 ;;    ,      (transduce (partition-all-t 3) conj (range 10)) ;; => [[0 1 2] [3 4 5] [6 7 8] [9]] ;; ... ! ;;   ( ) (transduce (comp (filter odd?) (partition-all-t 3)) conj (range 10)) ;; => [[1 3 5] [7 9]]
      
      





しかし、 reduce



代わりにtransduce



を使用しようとするとどうなりますか?



 (reduce (identity -) 0 [1 2 3 4]) ;; => -10 ;; ~~ (- (- (- (- 0 1) 2) 3) 4) (transduce identity - 0 [1 2 3 4]) ;; => 10 ;;  !
      
      





reduce



transduce



に直接置き換えることは不可能であることが判明しました-1項還元関数の新しい要件が干渉します。 この例では、計算が完了した後、呼び出しをtransduce



(- acc)



、結果の符号を逆にします。 完了すると、状況を修正できます。



 ((completing -) 3 2) ;; => 1 ((identity -) 1) ;; => -1 ((completing -) 1) ;; => 1 (transduce completing - 0 [1 2 3 4]) ;; => -10 ;;   !
      
      





トランスフォーマー、トランスデューサーを操作するための言語の中核には、特別な機能が登場しています。 また、これらの非常にトランスデューサーの標準のセットを追加することが期待されます。 そして、多くの新しい関数を生成しないように(それらが非常に多いため、2つのアカウントで混乱します)、既存のmap



filter



take



mapcat



mapcat



およびcompanyをアップグレードすることにしました。



 (map inc [1 2 3]) ;; => (2 3 4) (map inc) ;; => #<core$map$fn__4525 clojure.core$map$fn__4525@2d9e9038> ;;  ! ;;    (transduce (map inc) + [1 2 3]) ;; => 9 (transduce (comp (map inc) (filter even?)) + [1 2 3]) ;; => 6 ;; ~~ (+ (inc 1) (inc 3)) => 6
      
      





transduce



加えて、 transduce



操作するための機能がいくつかあります。



 ;;     (sequence (map inc) [1 2 3]) ;; => (2 3 4) ;;     (transduce (map inc) conj [1 2 3]) ;; => [2 3 4] ;; ... ;;  `sequence`   ** ! ;;  `transduce`      (take 5 (sequence (map inc) (range))) ;; => (1 2 3 4 5) ;;   `into`     (into [9] (map inc) [1 2 3]) ;; => [9 2 3 4]
      
      





しかし、最も面白い機能は教育です。 seq



を呼び出してjava-iteratorをreduce



か取得できるプロキシオブジェクトを返します。 このオブジェクトは、単にtransduce



またはsequnce



呼び出すことが期待されています。 些細なことですが、便利です。



 (def odds (eduction (filter odd?) (range))) (def evens (eduction (remove odd?) (range))) ;;     sequential (take 5 odds) ;; => (1 3 5 7 9) ;;     sequence   100500  ;;       - sequence  GC (nth odds 100500) ;; => 2010001 ;;       reduce (  LazyCol) ;; ~= (reduce ((filter even?) ((take 100500) +)) 0 (range)) (transduce (take 100500) + evens) ;; => 10100149500
      
      





停止、停止、停止。 clojure.core.reducers/reducer



思わせますが、最小化することしかできず、 seq



実行seq



許可seq



れていました。 r/reducer



はゴミ箱に捨てます! しかし、 r/folder



ではなく、彼はマルチスレッドの方法を知っています!



 (require '[clojure.core.reducers :as r]) (def v (vec (range 1000000))) (time (transduce (map inc) + v)) ;; "Elapsed time: 120.193971 msecs" ;; => 500000500000 (time (r/fold + (r/folder v (map inc)))) ;; "Elapsed time: 37.597224 msecs" ;; => 500000500000 ;;   ! (transduce (take 100500) + v) ;; => 5050074750 (r/fold + (r/reducer v (take 100500))) ;; => 5050074750 ;;  ;; reducer  -   eduction (r/fold + (eduction (take 100500) v)) ;; => 5050074750 (reduce + (r/folder v (take 100500))) ;; => 5050074750 ;;    (r/fold + (r/folder v (take 100500))) ;; => 109071345018 ;; ... ;;     (   )
      
      





トランスデューサーを使用すると、通常のmap



/ filter



/ etc



(遅延シーケンスに基づく)と比較して優れたパフォーマンスと、柔軟性/抽象性の両方が実現します。 ここでは、clojurシーケンスについて説明していることに注意してください。抽象化と速度の点では、トランスデューサは通常のイテレータ/列挙子/ジェネレータに匹敵します(異なる言語では異なる呼び方です)。



しかし、Clojureに戻ります。 以前のcore.asyncには、 map>



map<



filter<



filter>



などのような多くの関数がありました。 今日、それらは削除されました (削除されたので、これまで停止されただけです)。 しかし、彼らは、チャンネルを作成するときにトランストランスデューサーを指定することを許可しました:



 ;;      project.clj (require '[clojure.core.async :as a]) ;;     (def xf (filter odd?)) ;;     (def ch (a/chan 10 xf)) ;;      0  9     (a/onto-chan ch (range 10)) ;;     (a/<!! (a/into [] ch)) ;; => [1 3 5 7 9]
      
      





トランスデューサーは、緩衝されたチャンネルにのみ掛けることができます。 そして、要素がバッファに表示される前に、トランスデューサはそれを処理します。 あらゆる種類のパイプラインがあり、トランスデューサーでも動作します。



まとめると



さまざまなレジューサー/トランスデューサーはすべて、畳み込み演算の一般化です。 したがって、それらを使用するには、2つの引数を持つ還元関数が必要です。



2進オプションに加えて、0進オプションも決定することをお勧めします。畳み込みの初期状態が指定されていない場合に使用できます。 または、使用されない場合があります。元のコレクションが空でない場合、 reduce



は最初の要素を取ります。 しかし、 transduce



はそれほど意味のある動作をしません-初期状態が明示的に渡されるか、reduct関数の0項呼び出しが使用されます。



一方、 transduce



は還元関数からより多くを必要とします-1項オプションが必要です。 これは、一般的な場合、ほとんど何もしません。 真剣に、通常、この場合は([x] x)



が最も意味のある実装です。 しかし、私たちは怠け者で、古い関数(0/2項)を書き換えるのが面倒なので、空の1項オプションを追加するcompleting



ラッパーを使用します。



さらに、減速機は変圧器に基づいています。 Transformer = rf -> rf



タイプの関数。 実際、減速機は、変圧器がしっかりとねじ込まれたコレクションです。 そして、このコレクションでreduce



を実行reduce



たびに、最初にトランスフォーマーがreduct関数を「台無しにします」。



変換器〜=変換器、1項還元関数のサポートのみが必要です。 したがって、この最も不幸な1-arnikを常に定義し、誇らしげに宣言します。「もちろん、古いトランスは使用せず、トランスデューサーのみを使用します」。



このすべてにより、トランスデューサーはコレクションの操作に限定されません。 それらをチャネル、入出力ストリーム、キュー、オブザーバーなどに固定できます。 一般に、そのすべてのファンタジーには十分です。



合計:




All Articles