My first introduction to functional programming was a couple years ago when I read through the famous SICP. As someone who had up to this point worked with mostly in object oriented and imperative languages, I had rarely seen
reduce before that time. The purpose of the former two felt obvious; the latter one not so much. This blog post is geared for someone who knows how
reduce works but feels like they struggle to use it practically.
Feel free to skip this section if you understand the basics of reduce
Reduce is known as a higher order function (HOF). A HOF is defined as a function that either takes in or returns another function.
Reduce takes two parameters: the first is a function, and the second is known as the initial accumulator. We will invoke this function over every element of an array. The ultimate goal is to transform the array into something new.
Suppose we want to take an array of characters and concatenate them into a single string. In this example, the first argument we pass reduce is a HOF that will operate over every character. The HOF takes two arguments: the first is the accumulator and is the value that we ultimately want to assemble, and the second is a particular element of the array.
Let’s simulate what happens when we run a solution to the problem of concatenating strings.
You can use the below table to help guide yourself along
|invocation #||resultantString||nextCharacter||return value|
We will ultimately call our HOF 4 times, passing in each of the 4 elements in the initial array.
The first time we invoke the HOF, the first argument is always the initial accumulator. Notice the
"" passed into reduce above. The second argument is
'a' because that’s the first element in our array. We return ‘a’, and this return value is the
resultantString that is passed as an argument to the second invocation of our HOF, along with the next element of the array,
'b'. The process continues.
'ab' is returned from the HOF’s second invocation, and
'c' are passed into the third invocation of the HOF. Ultimately,
We built up the result step by step through a series of concatenation steps. Every call to the HOF will return a string with one additional character added to the previous accumulator.
I think one of the reasons
filter are easier than
reduce is because they always return an array. That’s not the case for
reduce is ultimately designed to transform one type into another.
When you begin writing a
reduce statement, your first question to yourself should be “What type am I returning?”. Here’s a hint, it’s probably one of the following:
Array. Once you know what type you will return you can begin writing your reduce statement. I say this because the initial accumulator (reduce’s second argument) can easily be determined by what type you expect
reduce to return. Here is a table to guide your decision.
|return type||initial accumulator|
Notice that the initial accumulator and the return type are always the same!
You probably won’t want return an array from a reduce function very often because chances are you could have more simply used a combination of
filter, and/or some other function instead.
So let’s say we want to reduce over a set of numbers and return the sum of all the odd numbers. Since we are returning a
number, we can use the above table to determine that the starting accumulator should be 0. That gives us a pretty decent starting point!
Now we just need to complete the guts of the function such that it increments the sum when the
nextNumber is odd. If
nextNumber is even, it should return
One interesting thing to note about the above code is that EVERY code path (every if-else block) returns something. This is a very common theme throughout the functional programming paradigm and holds true when writing reduce’s higher order function. In general it’s a decent litmus test to establish if you’ve made a bug somewhere. More specifically, if every code path does not return something, and if that something is not the same type as your accumulator, you probably have a bug.
Let’s do something a little different. Suppose I have an array of words, and I want to count how many times each word appears in the array. We can represent this information using an object that maps words in the array to a number indicating their frequency of occurrence.
Consulting the table above, we know that the starting accumulator needs to be an empty object. This means that each iteration over the array returns an object as well.
Every step of the higher order function needs to examine the next word and determine if it has been seen before. If it has not, then we need to add an entry to the accumulator and return it. If it has, then we need to increment the existing entry inside the accumulator object and return it.
Just like the first example, every code path returns the type of the accumulator.
Let’s end with something a little more complicated.
Suppose we want to implement a merge function to combines several objects into a single object. We’re going to implement something really similar to Object.assign. Here are some examples.
We know that the return value from this operation is an object, so that will be our starting accumulator. Every iteration of the array will produce a new object that contains the merging of the previous accumulator and the next object.
- Always start by asking what the type of your output is. The type of your accumulator will follow by using the mapping table.
- The logic inside your higher order function should assemble your resulting accumulator piecemeal. Since the return value of the previous iteration is the input to the next iteration, every code path must return a value
- Furthermore, unless you want a horrible experience, every code path MUST return a value whose type is the same as that of your initial accumulator.
At the end of the day, just about everything you can do with reduce can be done with some combination of
fitler and perhaps another functional method. And the alternative solution is almost always simpler and more readable. So, in practice, you probably don’t want to use
reduce that often. With that said,
reduce is a building block on which every other functional method can be built, and we will explore this unique trait in my next blog post.