Alec Grey
Posted on October 18, 2020
.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
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}
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
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
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"]
Well let's see what .reduce can do!
names.reduce do |longest, current|
if current.length > longest.length
current
else
longest
end
end #=> 'Radagast'
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 longest
for the next iteration.
This could be written more elegantly as well:
names.reduce do |longest, current|
current.length > longest.length ? current : longest
end #=> 'Radagast'
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"]
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"]
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(:+)
?
Posted on October 18, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.