Reputation: 125
I'm writing my own each
and reduce
functions. The reduce function receives the initial value of accumulator and a block applied to each element. The tests I must pass, however, have assertions without specifying the accumulator, it means that it must be set by default. The problem is that for multiplication the accumulator's initial value must be equal '1' and for addition - '0'.
My tests check the following:
func = -> (acc, element) { acc * element }
array.my_reduce(&func)
array.my_reduce(2, &func)
and also addition:
array.my_reduce(&:+)
I couldn't find a better way than doing it with hard-coded values:
def my_reduce(acc = 0)
acc += 1 if yield(3, 2) == 3 * 2 && acc == 0
my_each { |el| acc = yield(acc, el) }
acc
end
Is there a more elegant way of checking operation in the block or setting the accumulator's initial value conditionally?
Update: As @maxpleaner suggested, I used the first array item as the initial accumulator value, iterating the tail of the array. Now it looks like this:
def my_reduce(initial = nil)
acc = initial.nil? ? self[0] : initial
tail = initial.nil? ? self[1..(length - 1)] : self
tail.my_each { |el| acc = yield(acc, el) }
acc
end
Upvotes: 0
Views: 93
Reputation: 55778
A simplified version of inject
/ reduce
could look like this:
# A special undefined value which is the only value which
# can not be used in the Enumerable or as a memo. See below
# for considerations
UNDEFINED=Object.new
def inject(memo=UNDEFINED)
each do |element|
# If we didn't get an initial value, we use the first element
# but it won't be yielded in its own.
if UNDEFINED.equal?(memo)
memo = element
next
end
# In the first each loop (if we got an explicit memo) or
# in the second loop (if we didn't), we pass both value
# to the provided block and use the returned value as
# the new memo
memo = yield(memo, element)
end
# If the memo is still UNDEFINED here, it means that the `each`
# method above has not yielded any value, most likely because
# `self` is an empty enumerable (such as an empty Array or Hash).
# In that case, we return `nil`.
UNDEFINED.equal?(memo) ? nil : memo
end
This method was adapted from the implementation in Rubinius where they use a similar approach.
Compared to the method available on the Enumerable module, this one has a few restrictions. Most importantly, it doesn't support to pass the operator as a symbol instead of a block. With the Ruby's version, you could e.g. run [1,2,3].inject(:+)
and get 6
back.
In the code above, please note the technique to detect wether the user passed an argument. This is required since any value the user could explicitly pass (including nil
) would be a valid value for the memo
and would thus change the flow. As such, we have to specifically detect whether the user passed an argument at all (with any possible value), compared to the default case where they didn't pass an argument.
In MRI (the "default" Ruby implementation), they use a different technique in the C code as they count the number of passed arguments directly. You can learn more about these techniques on my blog at https://holgerjust.de/2016/detecting-default-arguments-in-ruby/
Upvotes: 1
Reputation: 26778
The default starting item of reduce is the first element in the array/enumerable.
If you special cased +
and *
to have default starting values of 0 and 1 respectively (not sure how you would do this, anyway), then [].reduce(&:+)
and [].reduce(&:*)
would also return 0 and 1 respectively. However, if you try in irb you will see that both expressions return nil.
Upvotes: 0