Reducers provide an alternative approach to using sequences to manipulate standard Clojure collections. Sequence functions are typically applied lazily, in order, create intermediate results, and in a single thread. However, many sequence functions (like map and filter) could conceptually be applied in parallel, yielding code that will get faster automatically as machines get more cores. For more details on the rationale for reducers, see the original blog posts.

reducer とは 畳み込み可能なコレクション (自分自身の畳み込み方を知っているコレクション) と 畳み込み関数 (畳み込みの際に必要な処理の "レシピ") の組み合せである。標準的なシーケンス操作は、実際の操作を行わない代わりに畳込み関数を変換するもので置き換えられる。操作の実行は最終的な畳込みが行われるまで保留される。これによってシーケンスの場合にあるような中間結果や遅延評価を取り除くことができる。

加えていくつかのコレクションは (persistent な ベクター や マップ) は フォールド可能 である。reducerに対する フォールド 操作は畳み込みを次のように並列に実行する:

  1. 畳み込み可能なコレクションを指定されたサイズに分割する (デフォルト = 512 要素)

  2. 各分割に対して reduce を適用する

  3. 再帰的に各分割を Javaの fork/join フレームワークを使用して結合する。

コレクションがフォールドをサポートしていない場合は非並列なreduceにフォールバックする。

reduce と fold

clojure.core.reducers の名前空間では (ここでは r としてエイリアスしている) r/reduce 関数の代わりを提供している。

(r/reduce f coll)
(r/reduce f init coll)

reducer のバージョンは次の点で異なる:

  • マップのコレクションは reduce-kv でreduceされる

  • init が与えられていない場合、 初期値を作り出すためにfが引数なしで呼び出される

    • 注意: 初期値を作り出すためにfが複数呼び出される場合がある

一般的にはほとんどのユーザーは、 r/reduce を直接呼び出す代わりに、並列なreduceと結合を実装する r/fold を使用するべきである。ただし、中間結果を削減する目的で貪欲なreduceを実行することは有用かもしれない。

(r/fold reducef coll)
(r/fold combinef reducef coll)
(r/fold n combinef reducef coll)

r/fold はreduce可能なコレクションを受け取り、それを要素数がほぼ n (デフォルト 512)のグループに分割する。各グループはreducef関数でreduceされる。reducef関数は 各分割で 初期値を作り出すため引数なしで呼び出される。それぞれのreduceの結果がさらにcombinef関数でreduceされる(defaultがreducef)。引数なしで.呼び出される場合にcombinef関数はその恒等値を作りだす必要がある - これは複数回呼び出される。操作は並列に実行されうる。結果は順序を保持する。

The following functions (analogous to the sequence versions) create reducers from a reducible or foldable collection: r/map r/mapcat r/filter r/remove r/flatten r/take-while r/take and r/drop. None of these functions actually transforms the source collection. To produce an accumulated result, you must use r/reduce or r/fold. To produce an output collection, use clojure.core/into to choose the collection type or the provided r/foldcat to produce a collection that is reducible, foldable, seqable, and counted.

reducerを使用する

fold を使用して + で和を求める:

(require '[clojure.core.reducers :as r])
(r/fold + (r/filter even? (r/map inc [1 1 1 2])))
;=> 6

into を使用して最終的なコレクションを作成する:

(into [] (r/filter even? (r/map inc (range 100000))))

もしくは r/foldcat:

(r/foldcat (r/filter even? (r/map inc (range 100000))))

foldのreduce関数とcombine関数を指定する:

(defn count-words
  ([] {})
  ([freqs word]
    (assoc freqs word (inc (get freqs word 0)))))

(defn merge-counts
  ([] {})
  ([& m] (apply merge-with + m)))

(defn word-frequency [text]
  (r/fold merge-counts count-words (clojure.string/split text #"\s+")))

使用するべき時

reducer formを使用するべき操作:

  • 効率の良い、積極的な複数ステップの変換の適用

  • 遅延シーケンスに見られるような、取り残されたI/Oリソースの問題を避ける

fold を使用するべき時:

  • ソースのデータが、メモリ内で生成でき、かつ保持できる

  • 処理の内容が計算である (I/O や ブロッキングではない)

  • 処理件数や行う処理が "大きい" 場合