.reduce: a versatile array method

alecgrey

Alec Grey

Posted on October 18, 2020

.reduce: a versatile array method

.reduce is a confusing enumerable. But I'd like to think that what makes it confusing can also make it a powerful tool.

Today I'd like to dispel some of the confusion, and also show some examples of versatile of a method it is.

What does .reduce do?

Let's start with an easy example: Let's say we have an array of numbers that we want to reduce down to a single value, it's sum. (Let's also say a powerful, Ruby-based wizard took away our .sum method)

array = [1, 1, 1]

array.reduce(0) {|sum, i| sum + i} #=> 3
Enter fullscreen mode Exit fullscreen mode

OK. Let's unpack what the code just did, above.

.reduce as an accumulator method

Above, we gave .reduce a single parameter (reduce(0)), as well as named two variables within the pipes (|sum, i|). Let's call the parameter the start value, and the two variables the memo, and current_element, respectively.

array.reduce(start) {|memo, current_element| statement}
Enter fullscreen mode Exit fullscreen mode

current_element is fairly straightforward; it is the current value of the array you are iterating over.

memo stores the return value of the previous iteration.

start provides an optional starting-point, by passing it's value as the first value of memo.

Let's look at the above example again, but this time at each iteration that is happening:

array = [1, 1, 1]

array.reduce(0) {|sum, i| sum + i}

# 1st: 0 + 1 #=> 1
# 2nd: 1 + 1 #=> 2
# 3rd: 2 + 1 #=> 3
Enter fullscreen mode Exit fullscreen mode

Each iteration is adding the sum value to the current_element, then saving that return as the next value for sum. At the end of the iteration, the final value of sum is returned!

Before we move on... remember how I said that the parameter value was optional? If we don't pass in the start value, the method will take array[0] for the first value of memo, and start iterating with array[1]!

array = [1, 1, 1]

array.reduce {|sum, i| sum + i} #=> 3
Enter fullscreen mode Exit fullscreen mode

Now that we understand the basics, let's get into some of the fun versatility that .reduce has to offer.

Using conditional logic to control our returns

Well, I guess I forgot my repellent. The pesky wizard came back and this time he took our max_by method... How am I supposed to find the largest name out of this group of names?

names = ["Gandalf", "Saruman", "Radagast"]
Enter fullscreen mode Exit fullscreen mode

Well let's see what .reduce can do!

names.reduce do |longest, current|
    if current.length > longest.length
        current
    else
        longest
    end
end #=> 'Radagast'
Enter fullscreen mode Exit fullscreen mode

By implementing some control-flow, we can look at each value of the names array, and either return the current if it's longer than the longest, or keep the same value of longestfor the next iteration.

This could be written more elegantly as well:

names.reduce do |longest, current|
    current.length > longest.length ? current : longest
end #=> 'Radagast'
Enter fullscreen mode Exit fullscreen mode

Ternary operators make up some of the bread-and-butter of .reduce control flow.

There is a glaring flaw in our method, however. What happens when two or more names are of the same length?

names = ["Bronson", "Dave", "Swanson", "Steve", "Johnson"]
Enter fullscreen mode Exit fullscreen mode

Our current .reduce method will only return the first longest name, since no subsequent values will pass true for current.length > longest.length!

Returning multiple values with .reduce

Remember how we can pass an optional start parameter to our method? Well we can actually use this to our advantage to create a .reduce method that returns an array of multiple responses!

names = ["Bronson", "Dave", "Swanson", "Steve", "Johnson"]

names.reduce([""]) do |long_names, current|
    if current.length > long_names[0].length
        [current]
    elsif current.length == long_names[0].length
        long_names << current
    else
        long_names
    end
end #=> ["Bronson", "Swanson", "Johnson"]
Enter fullscreen mode Exit fullscreen mode

To break this down, we need to keep the output consistent across each iteration, so since we want an array of strings, we put an array with an empty string as the start value. After each iteration, we can either start a new array if current is greater than the previous longest name, we can add to the array if current is the same length as the previous longest name, or we can move on, by returning the long_names array in it's current form.

.reduce is a powerful tool, so I hope this helped make it more clear how it works. There's still power yet to be explored with this enumerable, so as a cliffhanger, I'll leave you with another way to refactor the sum example from earlier:

Instead of array.reduce {|sum, i| sum + i}...

Why not try array.reduce(:+)?

💖 💪 🙅 🚩
alecgrey
Alec Grey

Posted on October 18, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related