3

Consider the following irb loop:

irb(main):015:0> [nil, nil].reduce(0) { |accum, x| accum + 1 unless x.nil? }
=> nil

Why does this return nil instead of 0?

According to the Ruby Enumerable documentation:

If you specify a block, then for each element in enum the block is passed an accumulator value (memo) and the element. If you specify a symbol instead, then each element in the collection will be passed to the named method of memo. In either case, the result becomes the new value for memo. At the end of the iteration, the final value of memo is the return value for the method.

My expectation would be that the accumulator should get set to 0 before the array starts to get folded, since that is given as the initial value. Then, the block's escape clause will trigger for all elements in this array, so the accumulator will never change. Finally, since 0 is the last value stored for the accumulator, it should be returned.

David Bodow
  • 688
  • 5
  • 15
  • "Then, the block's escape clause will trigger for all elements in this array" – And what is the return value of the block (and thus the value of the accumulator) in that case? – Jörg W Mittag Sep 24 '17 at 16:54
  • The short answer is that `accum + 1 unless x.nil?` is the same as `x.nil? ? nil : (accum + 1)`. – Cary Swoveland Sep 24 '17 at 18:18

3 Answers3

3

Whatever the block returns is going to be the next accumulator value.

And you return nil:

'whatever' unless true #=> nil

You could do this:

arr.reduce(0) { |a, e| e.nil? ? a : a + 1 }

Or this:

arr.compact.reduce(0) { |a, e| a + 1 }

Or this:

arr.compact.size
Danil Speransky
  • 29,891
  • 5
  • 68
  • 79
  • Makes perfect sense; thanks! To summarize the conceptual mistake I made: the accumulator is the return of each call to the block, not a boxed value in memory that gets changed by side effect and then returned at the end of the `reduce` call. – David Bodow Sep 24 '17 at 17:45
1

Reduce starts with the accumulator you pass but then sets it to whatever the block returns.

It might be helpful to see what it does internally (this is not the actual source code but a simple reproduction):

class Array
  def my_reduce(memo, &blk)
    each { |i| memo = blk.call(memo, i) }
    memo
  end
end

Here's some examples to show its usage:

# with 0 as starting memo
[nil, nil].reduce(0) { |memo, i| i ? memo + 1 : memo } # => 0
[nil, nil].reduce(0) { |memo, i | memo += 1 if i; memo; } # => 0
[nil, nil].reduce(0) { |memo, i| memo + (i ? 1 : 0) } # => 0

# with [] as starting memo
[1,2].reduce([]) { |memo, i| memo.push(i + 1); memo } # => [2,3]
[1,2].reduce([]) { |memo, i| memo.concat([i + 1]) } # => [2,3]
[1,2].reduce([]) { |memo, i| memo + [i + 1] } # => [2,3]
[1,2].reduce([]) { |memo, i| [*memo, i + 1] } # => [2,3]

You can see how only some of these require memo to be returned as the last line. The ones that don't are taking advantage of methods which return their modified objects, instead of relying on mutability (memo.push) or local variable assignment (memo += 1)

each_with_object is basically the same thing as reduce except that it automatically returns the accumulator from each block, and reverses the order of the block args (|i, memo| instead of |memo, i). It can be nice syntactic sugar for reduce when the memo is a mutable object. Returning the memo from the block is no longer necessary in the following example:

[1,2].each_with_object([]) { |i, memo| memo.push(i + 1) } # => [2,3]

However it won't work with your original example, because the memo (a number) is immutable:

# returns 0 but should return 1
[true, nil].each_with_object(0) { |i, memo| memo += 1 if i }

to say memo += 1 here is nothing but local variable assignment. Remember, you can never change the value of self for an object in ruby, not even a mutable one. If Ruby had increment operators (i++) then it might be a different story (see no increment operator in ruby)

max pleaner
  • 26,189
  • 9
  • 66
  • 118
0

Your expectations are correct but the missing piece is considering what the block returns after execution.

In Ruby the last thing to execute is returned and this code: accum + 1 unless x.nil? returns nil.

Just for science here's an example:

irb(main):051:0> puts 'ZOMG' unless nil.nil?
=> nil

Because nil is returned by the block your accumulator's initial 0 is overwritten with nil.

If you modify the code to return the accumulator you will get 0 as expected:

irb(main):052:0> [nil, nil].reduce(0) do |accum, x|
irb(main):053:1*     accum + 1 unless x.nil?
irb(main):054:1>     accum
irb(main):055:1> end
=> 0
Mario Zigliotto
  • 8,315
  • 7
  • 52
  • 71