A reduce Example Explained
From OO to Clojure Workshop!
Watch my free workshop to help you learn Clojure faster and shift your paradigm to functional.
Summary: A deep-dive into a single
reduce example shows how much can
happen in a short bit of code.
A few weeks ago I published a post with some annotated reduce examples. The idea was to explain using metaphor what reduce was doing and visually explain how those parts worked in the code.
I think it was pretty successful in general. But a reader asked me some great questions about one of the examples that got me thinking that I should explain it deeper.
Here is the example:
(reduce (fn [[n d] b] ;; `n` is the numerator, ;; `d` is the denominator [(+ n b) ;; add each number to the numerator (inc d)]) ;; add 1 to the denominator [0 0] ;; start here [1 2 3 4 5]) ;; average these numbers, one at a time
The real problem with this example is the number of things going on at
the same time. I don't want to distract people from the main point,
which was that
reduce itself is simple and easy to follow.
So now I'm going to focus on that example by itself, and annotate it even further.
First, notice that it has the normal reduce arguments: a function, an
initial value, and a collection. It might be confusing because the
initial value is itself a collection! This is a wonderful example of
something beautiful about
reduce: the first argument to the function,
the return value of the function, and the initial value should have
the same structure.
(reduce (fn [[n d] b] ;; a pair [(+ n b) (inc d)]) ;; a pair [0 0] ;; a pair [1 2 3 4 5])
In this example, they're all pairs. In Clojure, we represent pairs as
vectors of two elements. So
although the vector
[1 2 3 4 5] is also a vector, it's semantically
different. That one we treat as a sequence.
We can break this down a little further. We're using argument destructuring in the function:
(fn [[n d] b] ;; first `[` indicates arguments ;; second `[` indicates destructuring [(+ n b) (inc d)])
Destructuring in Clojure is a convenient way to name parts of a collection without writing all of the parts the long way. The above function could be rewritten like this:
(fn [avg b] ;; it's really two arguments (let [n (first avg) ;; name the first element d (second avg)] ;; name the second element [(+ n b) (inc d)])) ;; then return a pair of numbers
It's up to you how you want to write it. I like using destructuring. In a case like this, it's kind of a toss-up. As the destructuring gets longer, you save more. It's also a great way to indicate what your function expects as an argument. The longer way is not as clear. Two lines of naming distract from one line doing work.
Now, there's one more thing about this function. The return value is a
vector using the literal vector syntax, acting as a pair. The two
values of the pair will be evaluated, crammed into a vector, and
returned. That pair will hold two numbers. This is really just like
[0 0], except instead of literal
0s, we have two expressions that
evaluate to numbers.
So let's go through the whole example once more:
(reduce (fn [[n d] b] ;; 2 args: pair of numbers (`n` and `d`) ;; and a number (`b`) [(+ n b) ;; return a vector with `n + b` (inc d)]) ;; and `d + 1` [0 0] ;; initial pair passed as 1st argument [1 2 3 4 5]) ;; the numbers to be passed as `b`
[0 0] in your left hand. Walk down the list. When you get to
pick it up in your right hand. Now, add what's in your right hand to the
first number in your left hand, and add 1 to the second number in your
left hand. Hold the two new numbers in your left hand. Proceed down the
line, picking each number up in your right hand, until the end. The
answer is in your left hand.
At each step, you start with a pair, you do something to it, and you end with a pair. When you're done, you have a pair that is the answer.
Well, I hope that these explanations helped you see a little bit better how to read Clojure code. There are quite a few things to learn, and it takes a little practice, but reading or writing an expression like this does become second nature. That's why I so casually threw it out there, like it was self-explanatory. Sorry!
If you're into functional programming and I didn't scare you off from Clojure, you might be interested in LispCast Introduction to Clojure. It covers the basic syntax with a step-by-step sequence of exercises to make it second nature. It's fun: you get to teach a robot how to bake!