Search code examples
clojure

How to write custom reduction function with 0 and 2-ary cases


The following example in clojure calls the nullary and binary cases of + in various ways:

(println 101 (+)) ; fine
(println 102 (+ (+) 4)) ; fine

(println 103 (reduce + (+) (range 4))) ; fine
(println 104 (reduce + (range 4))) ; fine

I tried replacing + with mean-reducer as described in this blog post (warning: no https).

I changed the mean-reducer function to explicitly expose its identity element {:sum 0 :count 0} when invoked with no arguments.

This works fine for the simple cases leading up to (reduce mean-reducer (range 4)), but falls over for (reduce mean-reducer (range 4)) itself.

(defn mean-reducer
  ([] {:sum 0 :count 0})
  ([memo x]
         {
             :sum (+ x (memo :sum))
             :count (inc (memo :count))
         }))

(println 201 (mean-reducer)) ; fine
(println 202 (mean-reducer (mean-reducer) 4)) ; fine

(println 203 (reduce mean-reducer (mean-reducer) (range 4))) ; fine
;; (println 204 (reduce mean-reducer (range 4))) ; bad

runs and produces this with the last line commented.

% clojure mean_reducer.clj
201 {:sum 0, :count 0}
202 {:sum 4, :count 1}
203 {:sum 6, :count 4}

The error message and stack trace associated with the failed call to (reduce mean-reducer (range 4)) looks like this:

(~/clojure/mean_reducer.clj:12:62).
    at clojure.lang.Compiler.load(Compiler.java:7647)
    at clojure.lang.Compiler.loadFile(Compiler.java:7573)
    at clojure.main$load_script.invokeStatic(main.clj:452)
    at clojure.main$script_opt.invokeStatic(main.clj:512)
    at clojure.main$script_opt.invoke(main.clj:507)
    at clojure.main$main.invokeStatic(main.clj:598)
    at clojure.main$main.doInvoke(main.clj:561)
    at clojure.lang.RestFn.applyTo(RestFn.java:137)
    at clojure.lang.Var.applyTo(Var.java:705)
    at clojure.main.main(main.java:37)
Caused by: java.lang.ClassCastException: class java.lang.Long cannot be cast to class clojure.lang.IFn (java.lang.Long is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
    at user$mean_reducer.invokeStatic(mean_reducer.clj:5)
    at user$mean_reducer.invoke(mean_reducer.clj:1)
    at clojure.lang.LongRange.reduce(LongRange.java:222)
    at clojure.core$reduce.invokeStatic(core.clj:6823)
    at clojure.core$reduce.invoke(core.clj:6810)
    at user$eval143.invokeStatic(mean_reducer.clj:13)
    at user$eval143.invoke(mean_reducer.clj:13)
    at clojure.lang.Compiler.eval(Compiler.java:7176)
    at clojure.lang.Compiler.load(Compiler.java:7635)
    ... 9 more

I think this means that somehow the lines are getting crossed and an element of (range 4) is being bound to memo, but I'm not sure why this would happen given that the case with an explicit initial element succeeds.


Solution

  • When you pass no initial value to reduce the 2-arity version of mean-reducer is invoked with the first two elements of the range i.e. (mean-reducer 0 1). From reduce docs:

    If val is not supplied, returns the result of applying f to the first 2 items in coll, then applying f to that result and the 3rd item, etc.

    You'll need to supply an initial value to reduce if you want to use mean-reducer (and its 0-arity won't be used). reduce has two different "contracts" for its 2-arity reducing function depending on whether an initial value is specified.

    clojure.core.reducers

    clojure.core.reducers/reduce has exactly the behavior you want when no initial value is supplied:

    When init is not provided, (f) is used.

    (r/reduce mean-reducer (range 4))
    => {:sum 6, :count 4}