PurelyFunctional.tv Newsletter 313: Always use the 3-argument version of reduce

Issue 313 - February 11, 2019 · Archives · Subscribe

Clojure Tip 💡

Clojure Tip: Always use the 3-argument version of reduce. The 2-argument version can have unexpected behavior.

Do you use reduce? You should. It's one of the three functional tools (along with map and filter) that I made one of my first courses about. The production quality is not as high as my newer courses, but the content is good. Check it out. It's free.

The three-argument version of reduce is just great. However, the two-argument version has unexpected behavior. Here's an example. Feel free to try these at a REPL.

(reduce str [])    ;=> ""
(reduce str [1 2]) ;=> "12"

Given those expressions and their results, what would you expect from this?

(reduce str [1]) ;=> ????

From the examples above, I would expect "1". But guess what I get? 1 the Long, not a String.

Why?

The reduce docstring explicitly says If coll has only 1 item, it is returned and f is not called.

How could that ever work?

Well, in defense of this choice, it does work for lots of cases. For instance:

(reduce + [1])  ;=> 1
(reduce * [10]) ;=> 10

It works just fine when you don't need to call f to get the right answer. But if f changes the type, or does any kind of calculation, you will get the wrong answer. It's this kind of tricky, corner-casey behavior can mess you up. It works just great until it doesn't.

By why does Clojure do that?

This behavior of reduce was taken from Common Lisp. Rich Hickey has expressed his regrets about including it in Clojure (transcript here).

So what should I do instead?

The best and easiest change is to always use the three-argument version of reduce. The third argument is the initial value, which is often the identity of the function. In the case of the function str, it's the empty string. In the case of +, it's 0. In the case of *, it's 1. When a function has a value like this, it's called the identity of that function. Not all functions have an identity.

And you won't always start with the identity. Sometimes you start from somewhere else. reduce lets you do that because it's just an argument. You can start from wherever you want.

reduce is very practical and powerful. It's considered universal recursion over lists. That means that you can define any other recursive list operation using reduce. For instance, map and filter can be built using reduce. In fact, we do just that in the Reduce Mini-Course. This course has a ton of stuff you may want to learn about reduce, including how to use it, how implement it, and some complementary functions.

Clojure News ⚡️

The State of Clojure 2019 results are in. Cognitect has a blog post analyzing the results, written by Alex Miller. Daniel Compton did his own analysis. He has done this for a few years in a row. He organizes the free responses and picks ones that stand out. I appreciate the qualitative understanding of people's perceptions of Clojure. The Apropos panel talked about this, as well.

I consider this survey an important source of information about where I can add value to Clojurists---especially this rank ordering of "frustrations".

And I appreciate that 96% of respondents are happily using Clojure.

Brain skills 😎

There's a lot of scientific evidence that memories are formed and consolidated during REM sleep. Be sure to get plenty of good sleep when you're learning new material, like Clojure. And sleep is important for your health in general, so get plenty! For more information, read Why We Sleep by Matthew Walker. It's a great introduction to all the benefits of sleep and all the dangers of sleep deprivation.

Clojure Teaser 🤔

Here's a nice puzzle for you. I'll reveal the answer in the next issue.

Here is some code to print out a counter.

(def counter (atom 0))

(defn print-counter []
  (let [counter @counter]
    (println "===========")
    (println "| COUNTER |")
    (println (format "| %07d |" @counter))
    (println "==========")))

But when I run it at the REPL, I get an error message about Futures. Am I using Futures? Is there something going on behind the scenes?

user=> (print-counter)
===========
| COUNTER |
Execution error (ClassCastException) at user/print-counter (REPL:5).
class java.lang.Long cannot be cast to class java.util.concurrent.Future
(java.lang.Long and java.util.concurrent.Future are in module java.base of
loader 'bootstrap')

Can you figure out what's going on? Send me your answers and I'll share the answer next week.

See you then. And rock on! Eric Normand