A reduce Example Explained

August 10, 2015

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 b)
           (inc d)])
        [0 0]
        [1 2 3 4 5])
n is the numerator, d is the denominator
add each number to the numerator
add 1 to the denominator
start here
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]
          [(+ n b) (inc d)])
        [0 0]
        [1 2 3 4 5])
a pair
a pair
a pair

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]
  [(+ n b) (inc d)])
first [ indicates arguments; second [ indicates destructuring

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]
  (let [n (first avg)
        d (second avg)]
    [(+ n b) (inc d)]))
see, it's really two arguments
name the first element
name the second element
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, there's more of a savings. 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]
          [(+ n b)
           (inc d)])
        [0 0]
        [1 2 3 4 5])
2 arguments: pair of numbers (n and d) and a number (b)
return a vector with n + b
  and d + 1
initial pair of numbers passed as 1st argument
the numbers to be passed as b, 2nd argument

Hold [0 0] in your left hand. Walk down the list. When you get to 1, 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.

Conclusions

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 an 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!

You might also like