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!