Notes and examples of Transducers and Reducers
-
Used on collections
-
Eager evaluation only. (not lazy)
-
The operation(s) and the input are no longer tied together
(r/map zero?)
;=> #<reducers$map$fn__2527 clojure.core.reducers$map$fn__2527@7201f000>
(let [map-zero? (r/map zero?)
inputs [0 1 0 1 0 1]]
(reduce conj [] (map-zero? inputs)))
;=> [true false true false true false]
- Can be combined into a single function for better performance. (composable)
(def zero?-and-not-false
(comp
(r/filter true?)
(r/map zero?)))
;=> #'user/zero?-and-not-false
(reduce conj [] (zero?-and-not-false [0 1 0 1 0 1]))
[true true true]
-
Can be used in ClojureScript
-
Function C is the reducing function
-
Function B calls Function C
-
Function A creates Function B
-
(A C) -> B
(defn a [c]
(fn b
([] (c))
([coll] (c coll))
([coll input] (c coll input))))
;=> #'user/a
(transduce a + [1 2 3])
;=> 6
-
Doesn't care about the input type
-
Transducers seem to be faster than reducers
(dotimes [n 5] (time (r/reduce + 0 [1 2 3])))
"Elapsed time: 0.23142 msecs"
"Elapsed time: 0.047252 msecs"
"Elapsed time: 0.043944 msecs"
"Elapsed time: 0.062372 msecs"
"Elapsed time: 0.05938 msecs"
;=> nil
(dotimes [n 5] (time (transduce a + 0 [1 2 3])))
"Elapsed time: 0.1257 msecs"
"Elapsed time: 0.026548 msecs"
"Elapsed time: 0.018166 msecs"
"Elapsed time: 0.031276 msecs"
"Elapsed time: 0.024773 msecs"
;=> nil- Collection fns of previous Clojure versions are now optionally Transducers
(let [*2 #(* % 2)
*4 #(* % 4)]
(def weird-composition
(comp
(filter even?)
(map *2)
(map *4))))
;=> #'user/weird-composition
(into [] weird-composition [1 2 3 4])
;=> [16 32]
- For lazy transformations you must use
sequence
(def star-wrap
(map #(str "*" % "*")))
;=> #'user/star-wrap
(into [] star-wrap [1 2 3])
;=> ["*1*" "*2*" "*3*"]
(sequence star-wrap [1 2 3])
;=> ("*1*" "*2*" "*3*")
(type (into [] star-wrap [1 2 3]))
;=> clojure.lang.PersistentVector
(type (sequence star-wrap [1 2 3]))
;=> clojure.lang.LazyTransformer
Two comments, based on looking at the HEAD Clojure codebase.
Eagerness
I think that reducers are not eager per se, but many times that you think you have a reducer, you actually are getting a folder. For example, from
reducers.clj:returns a
folderwhilereturns a
reducer, and thereducer'scoll-reducejust defers to the collection'scoll-reduce:while
folderimplementsCollFoldas well:Not defining it for itself,
reducer'scoll-foldwill be inherited fromObject, where it is defined asIn summary, I believe that
foldwill be eager when and only when operating onfolders, while every other combination will be only as eager as the underlying collection.transduce vs reduce
Regarding the speediness of
core/transducevsr/reduceandcore/reduce,this seems to follow from a definition that
makes use of java implementations of
reduce, as specified by theIReduceinterface:In
PersistentList.java, we find:Honestly, I can't reason out the logic here, but for some reason
transduceends up using Java implementations of.reduce, whilereduceuses Clojure implementations ofcoll-reduce.