
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でこの歓迎を待っています(少し残っています)。
簡単に説明すると、トランスデューサーは同じ名前のトランス
(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 ;; !
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を常に定義し、誇らしげに宣言します。「もちろん、古いトランスは使用せず、トランスデューサーのみを使用します」。
このすべてにより、トランスデューサーはコレクションの操作に限定されません。 それらをチャネル、入出力ストリーム、キュー、オブザーバーなどに固定できます。 一般に、そのすべてのファンタジーには十分です。
合計:
- , ;
- , I/O, —
reducers
; -
reduce
—transduce
; - — ;
- , … ;
- —
map
,filter
.