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