DEV Community

loading...

The Good, the Bad, and the Accumulators.

Cora Sutton
keyboard cowgirl, baby hacktress
・3 min read

In programming it's common to need to accumulate a value. How you go about that in Clojure is a bit different from many other languages. In this post we'll go over some good ways and some not-so-good ways to accumulate values in Clojure.

The Not-so-good

Def in a function

If you're used to assigning to a value and then incrementing it, you might try something like this. Using def in a function like this, however, actually sets the value globally. If you have two threads calling this function at the same time you're going to get some wonky behavior.

(defn my-fn1 []
  (def my-variable1 1)
  (doseq [x (range 0 10)]
    (def my-variable1 (+ my-variable1 x))))

(my-fn1)

(println "oops, my-variable1 is" my-variable1)
;; oops, my-variable is 46
Enter fullscreen mode Exit fullscreen mode

Using an atom

Atoms are great! So are refs! They're really for safely changing data from multiple threads safely, though. There's an expense to using them that you just don't want to pay unless you're using multiple threads and so it's best to avoid them in cases like this.

(defn my-fn2 []
  (let [my-variable2 (atom 1)]
    (doseq [x (range 0 10)]
      (swap! my-variable2
             (fn [current-value]
               (+ current-value x))))
    (println "hi, my-variable2 is" @my-variable2)))

(my-fn2)
;; hi, my-variable2 is 46
Enter fullscreen mode Exit fullscreen mode

Volatile!

Volatile does work in this case but while volatile is faster than atoms it also has performance implications.

(defn my-fn3 []
  (let [my-variable3 (volatile! 1)]
    (doseq [x (range 0 10)]
      (vswap! my-variable3 (fn [current-value] (+ current-value x))))
    (println "hello, my-variable3 is" @my-variable3)))

(my-fn3)
;; hello, my-variable3 is 46
Enter fullscreen mode Exit fullscreen mode

The Good

Reduce v1!

Reduce! Now we're getting Clojure-y! This is a typical way to accumulate values in Clojure. You can do this with hashmaps, maps, sums, sets, etc, really anything at all. Reduce will go through each value in the range and pass in the previous accumulator value (or the starting value) and the current value in the range. The value returned from the function passed to reduce will be the new value for the accumulator. Once it runs out of values in the range the current value of the accumulator will be returned.

Also, you can skip sending a starting value and then the first accumulator value is the first value in the range!

(defn my-fn4 []
  (let [starting-value 1
        my-variable4 (reduce (fn [accumulator value]
                               (+ accumulator value))
                             starting-value
                             (range 0 10))]
    (println "omg, my-variable4 is" my-variable4)))

(my-fn4)
;; omg, my-variable4 is 46
Enter fullscreen mode Exit fullscreen mode

In this case we can just pass + as the function.

(defn my-fn5 []
  (let [starting-value 1
        my-variable5 (reduce +
                             starting-value
                             (range 0 10))]
    (println "yay, my-variable5 is" my-variable5)))

(my-fn5)
;; yay, my-variable5 is 46
Enter fullscreen mode Exit fullscreen mode

Loop/recur

Loop/recur is another essential way to handle this problem in Clojure. While I might not use it for adding up numbers I will use it for all kinds of other accumulations or complex behavior when looping over a sequence of values.

(defn my-fn6 []
  (loop [my-variable6 1
         values (range 0 10)]
    (let [value (first values)]
      (if value
        (recur (+ my-variable6 value)
               (rest values))
        (println "so, ok, my-variable6 is" my-variable6)))))

(my-fn6)
;; so, ok, my-variable6 is 46
Enter fullscreen mode Exit fullscreen mode

The Conclusion

It takes a little getting used to but eventually it feels a little uncontrolled and bug-prone to accumulate values the way some other languages do it.

Discussion (0)