【问题标题】:Why doesn't reduce return the initial value if the block never changes the accumulator?如果块从不更改累加器,为什么 reduce 不返回初始值?
【发布时间】:2018-03-05 15:28:15
【问题描述】:

考虑以下 irb 循环:

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

为什么返回 nil 而不是 0

根据 Ruby Enumerable 文档:

如果你指定一个块,那么对于 enum 中的每个元素,块都会传递一个累加器值(备忘录)和元素。如果你指定了一个符号,那么集合中的每个元素都将被传递给 memo 的命名方法。无论哪种情况,结果都会成为 memo 的新值。在迭代结束时,memo 的最终值就是方法的返回值。

我的期望是在数组开始折叠之前将累加器设置为0,因为这是作为初始值给出的。然后,该块的转义子句将针对该数组中的所有元素触发,因此累加器永远不会改变。最后,由于0 是为累加器存储的最后一个值,因此应该返回它。

【问题讨论】:

  • “那么,块的转义子句将触发这个数组中的所有元素”——在这种情况下,块的返回值(以及累加器的值)是什么?
  • 简短的回答是accum + 1 unless x.nil?x.nil? ? nil : (accum + 1) 相同。

标签: ruby


【解决方案1】:

无论块返回什么都将是下一个累加器值。

然后你返回nil:

'whatever' unless true #=> nil

你可以这样做:

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

或者这个:

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

或者这个:

arr.compact.size

【讨论】:

  • 非常有意义;谢谢!总结一下我犯的概念错误:累加器是每次调用块的返回值,而不是内存中的装箱值,它会因副作用而改变,然后在 reduce 调用结束时返回。
【解决方案2】:

Reduce 开始使用您传递的累加器,然后将其设置为块返回的任何内容。

看看它在内部做了什么可能会有所帮助(这不是实际的源代码,而是一个简单的复制品):

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

这里有一些例子来展示它的用法:

# 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]

您可以看到其中只有一些需要 memo 作为最后一行返回。那些没有利用返回其修改对象的方法,而不是依赖可变性(memo.push)或局部变量赋值(memo += 1)

each_with_object 与 reduce 基本相同,只是它自动从每个块返回累加器,并反转块 args 的顺序(|i, memo| 而不是|memo, i)。当备忘录是可变对象时,它可以成为减少的很好的语法糖。在以下示例中不再需要从块中返回备忘录:

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

但是它不适用于您的原始示例,因为备忘录(数字)是不可变的:

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

这里说memo += 1 只不过是局部变量赋值。请记住,您永远不能更改 ruby​​ 中对象的 self 的值,甚至不能更改可变对象。如果 Ruby 有自增运算符 (i++),那么情况可能就不同了(参见 no increment operator in ruby

【讨论】:

  • 有见地;谢谢!以后我也会记住each_with_object
【解决方案3】:

您的期望是正确的,但缺少的部分是考虑块在执行后返回的内容。

在 Ruby 中,最后一个要执行的东西被返回,这段代码:accum + 1 unless x.nil? 返回nil

只是为了科学,这里有一个例子:

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

因为nil 由块返回,所以您的累加器的初始0nil 覆盖。

如果您修改代码以返回累加器,您将按预期获得0

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

【讨论】:

    猜你喜欢
    • 2018-06-12
    • 2017-06-12
    • 1970-01-01
    • 2021-11-01
    • 1970-01-01
    • 2013-04-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多