This is pretty advanced stuff, but let’s see what I can do to help. Let’s start with an example of Ruby’s #reduce method, using the array and arguments in example 1 in your assignment:
[1, 2, 3, 4].reduce(0) { |sum, element| sum + element }
Now, here are the things to know. First, #reduce iterates through each of the values in an array and does whatever the block says to do with them. The block takes two variables. The first one (sum) is the “accumulator.” The accumulator starts equal to whatever value is passed in as an argument to the #reduce method (usually 0, as it is here). It holds the current total. The other variable (element) is assigned to each element, one at a time, one after the other (one per iteration, in other words). So, sum + element adds each element’s value to sum, one at a time.
The return value of the #reduce method is the accumulator variable, in this case sum. So, the code above will return 10, the sum of the four values in the array.
Now, how do you implement this functionality yourself? First, you create a method called reduce. This won’t have the same syntax as the #reduce method (to do that, you would have to “monkey patch” the Array class and override the existing method, and we don’t want to do that), but it will do the same thing. It will have two arguments, the array and the starting value for the accumulator.
You need also to understand up front that any method can take a block argument, without the method having to specify one the way it does with regular arguments. Also, calls to #reduce usually have a block argument, but if not, #reduce returns the input array converted to an Enumerator object.
All right. Let’s start with our method:
def reduce(arr, acc = 0)
return arr.to_enum unless block_given?
end
So far, so good. If the caller doesn’t provide a block argument, then return an Enumerator instead of raising an error. Also, we’ve put a default of 0 for the accumulator’s start value, since that’s what it usually is. Now for the hard part.
We want to run through each element of the array, and let the block operate on it, just like in the actual #reduce method. To do this, we’ll use while and the yield method:
def reduce(arr, acc = 0)
return arr.to_enum unless block_given?
counter = 0
while counter < arr.size
acc = yield acc, arr[counter]
counter += 1
end
acc
end
p reduce([1, 2, 3, 4]) { |sum, element| sum + element } # => 10
This is a pretty standard use of a while loop to iterate through the elements of an array, which shouldn’t be unfamiliar to anyone with experience in any language. But that yield might be new, so I’ll explain it a bit.
The Proc object’s yield method “invokes” (calls, sort of) the block argument and passes its arguments to it. If the method with yield in it is called without a block argument, yield will raise an error, unless provisions are made for that by using the #block_given? method to check whether a block argument is available.
The reason for yielding to blocks is that doing so lets you give back control of the program flow to whatever called your method. So, you can write your method to do some basic thing — iterating, for example — and let the caller decide the specifics of how to use the method. As you know, there are several Ruby methods that do some form of iteration and execute a block for each iteration: #each, #select, #reduce, etc.
That’s what we’re doing here. We yield each element to the block one at a time (along with the accumulator, which holds the result of yielding previous elements to the block), and set acc to the value that the block returns (in the case of our block, that’s sum + element, since that’s the last line of code in the block). When we’re all done iterating, we return the final value of acc to the caller. That’s pretty much it.
Now, we can pass in the same block to our reduce method that we pass to the regular #reduce method, and get the same result. In fact, this code can handle any of the blocks that your exercises provide (including the last one where they call it from inside an implementation of the select method), but they do throw you a curve on exercise 2. In this exercise, they’re passing in a range instead of an array — which you can also do with Ruby’s #reduce method. So, we need to add in one more line of code:
def reduce(arr, acc = 0)
return arr.to_enum unless block_given?
arr = arr.to_a # <<<<<<< This one
counter = 0
while counter < arr.size
acc = yield acc, arr[counter]
counter += 1
end
acc
end
You can safely call #to_a here, because reduce expects an array, or at least something that can be converted to an array. Also, calling #to_a on an array has no effect, so you’re safe calling it on every input.
There’s also an error in exercise 2; as written, it will return 6 instead of 10. To make it return 10, remove one of the periods from the range definition. The rule is that, for example, 1..4 is one through four inclusive, while 1...4 is one to three, not including four. Two dots includes the right number in the range, three dots excludes it. (The three dots can be useful in things like 0...some_array.size instead of 0..(some_array.size - 1), to create a range of all the indexes in an array.)
Hope this helps!