Clojure: First steps with reducers
I've been playing around with Clojure a bit today in preparation for a talk I'm giving next week and found myself writing the following code to apply the same function to three different scores:
(defn log2 [n] (/ (Math/log n) (Math/log 2))) (defn score-item [n] (if (= n 0) 0 (log2 n))) (+ (score-item 12) (score-item 13) (score-item 5)) 9.60733031374961
I'd forgotten about folding over a collection but quickly remembered that I could achieve the same result with the following code:
(reduce #(+ %1 (score-item %2)) 0 [12 13 5]) 9.60733031374961
The added advantage here is that if I want to add a 4th score to the mix all I need to do is append it to the end of the vector:
(reduce #(+ %1 (score-item %2)) 0 [12 13 5 6]) 12.192292814470767
However, while Googling to remind myself of the order of the arguments to reduce I kept coming across articles and documentation about reducers which I'd heard about but never used.
As I understand they're used to achieve performance gains and easier composition of functions over collections so I'm not sure how useful they'll be to me but I thought I'd give them a try.
Our first step is to bring the namespace into scope:
(require '[clojure.core.reducers :as r])
Now we can compute the same result using the reduce function:
(r/reduce #(+ %1 (score-item %2)) 0 [12 13 5 6]) 12.192292814470767
So far, so identical. If we wanted to calculate individual scores and then filter out those below a certain threshold the code would behave a little differently:
(->>[12 13 5 6] (map score-item) (filter #(> % 3))) (3.5849625007211565 3.700439718141092) (->> [12 13 5 6] (r/map score-item) (r/filter #(> % 3))) #object[clojure.core.reducers$folder$reify__19192 0x5d0edf21 "clojure.core.reducers$folder$reify__19192@5d0edf21"]
Instead of giving us a vector of scores the reducers version returns a reducer which can pass into reduce or fold if we want an accumulated result or into if we want to output a collection. In this case we want the latter:
(->> [12 13 5 6] (r/map score-item) (r/filter #(> % 3)) (into )) (3.5849625007211565 3.700439718141092)
With a measly 4 item collection I don't think the reducers are going to provide much speed improvement here but we'd need to use the fold function if we want processing of the collection to be done in parallel.
One for next time!