在数组上使用累加器映射

Map with accumulator on an array

我想为 Enumerable 创建一个方法,它同时执行 mapinject。例如,调用它 map_with_accumulator,

[1,2,3,4].map_with_accumulator(:+)
# => [1, 3, 6, 10]

或字符串

['a','b','c','d'].map_with_accumulator {|acc,el| acc + '_' + el}
# => ['a','a_b','a_b_c','a_b_c_d']

我无法找到有效的解决方案。我想我可以用 reduce 来做。我正在沿着这样的道路前进:

arr.reduce([]) {|acc,e| ..... }

初始值是一个空数组,但我没弄对。

编辑: 请参阅下面 Jörg 的回答以获得正确的解决方案。在阅读他的回答后我意识到另一种(有点粗暴)的方法是使用 instance_eval,它将给定块的上下文更改为执行它的对象的上下文。所以 self 被设置为引用数组而不是调用上下文(这意味着它不再是闭包!)并且 injectshift 被数组调用。令人费解,不必要的简洁,读起来令人困惑,但它教会了我一些新东西。

['a','b','c','d'].instance_eval do
  inject([shift]) {|acc,el| acc << acc.last+el}
end
#=> ['a','ab','abc','abcd']

这是使用reduce

的方法
['a','b','c','d'].reduce([]){|acc, e| acc << (acc == []?e:acc.last+'_'+e)}

您可以按如下方式进行:

module Enumerable
  def map_with_accumulator(sym)
    each_with_object([]) do |e,arr|
      arr <<
        if block_given?
          arr.empty? ? yield(first) : arr.last.send(sym, yield(e))
        else
          arr.empty? ? e : arr.last.send(sym,e)
        end
    end
  end
end

[1,2,3,4].map_with_accumulator(:+)             #=> [1,  3,  6, 10] 
[1,2,3,4].map_with_accumulator(:-)             #=> [1, -1, -4, -8] 
[1,2,3,4].map_with_accumulator(:*)             #=> [1,  2,  6, 24] 
[1,2,3,4].map_with_accumulator(:/)             #=> [1,  0,  0,  0] 

[1,2,3,4].map_with_accumulator(:+, &:itself)   #=> [1,  3,  6, 10] 
[1,2,3,4].map_with_accumulator(:-, &:itself)   #=> [1, -1, -4, -8] 
[1,2,3,4].map_with_accumulator(:*, &:itself)   #=> [1,  2,  6, 24] 
[1,2,3,4].map_with_accumulator(:/, &:itself)   #=> [1,  0,  0,  0] 

[1,2,3,4].map_with_accumulator(:+) { |e| 2*e } #=> [2,  6, 12,  20] 
[1,2,3,4].map_with_accumulator(:-) { |e| 2*e } #=> [2, -2, -8, -16] 
[1,2,3,4].map_with_accumulator(:*) { |e| 2*e } #=> [2,  8, 48, 384] 
[1,2,3,4].map_with_accumulator(:/) { |e| 2*e } #=> [2,  0,  0,   0] 

[1,2,3,4].to_enum.map_with_accumulator(:+) { |e| 2*e } #=> [2,  6, 12,  20] 
(1..4).map_with_accumulator(:+) { |e| 2*e }            #=> [2,  6, 12,  20] 
{a: 1, b: 2, c: 3, d: 4}.map_with_accumulator(:+) { |_,v| 2*v }
  #=> [2,  6, 12,  20] 

这个操作被称为scan or prefix_sum,但不幸的是,Ruby核心库或标准库中没有实现。

但是,您的直觉是正确的:您可以使用Enumerable#inject实现它。 (其实Enumerable#inject是通用的,次迭代操作都可以用inject来实现!)

module Enumerable
  def scan(initial)
    inject([initial]) {|acc, el| acc << yield(acc.last, el) }
  end
end

[1,2,3,4].scan(0, &:+)
# => [0, 1, 3, 6, 10]

%w[a b c d].scan('') {|acc, el| acc + '_' + el }
# => ["", "_a", "_a_b", "_a_b_c", "_a_b_c_d"]

理想情况下,行为应该与 inject 的行为相匹配,它有 4 个重载(在这种情况下,它会给你指定的结果),但不幸的是,在 Ruby 中实现这些重载,没有对 VM 内部结构(特别是发送站点的参数)的特权访问是后半部分的主要痛苦。

事情是这样的:

module Enumerable
  # Trying to match the signature of `inject` without access to the VM internals
  # is a PITA :-(
  def scan(initial=(initial_not_given = true; first), meth=nil)
    raise ArgumentError, 'You can pass either a block or a method, not both.' if block_given? && meth
    return enum_for(__method__) if initial_not_given && !meth && !block_given?
    return enum_for(__method__, initial) unless initial.is_a?(Symbol) || meth || block_given?
    meth, initial, initial_not_given = initial, first, true unless initial_not_given || meth || block_given?
    raise ArgumentError, "Method #{meth.inspect} is not a Symbol." unless meth.is_a?(Symbol) || block_given?

    this = if initial_not_given then drop(1) else self end

    return this.inject([initial]) {|acc, el| acc << acc.last.__send__(meth, el) } unless block_given?
    this.inject([initial]) {|acc, el| acc << yield(acc.last, el) }
  end
end

[1,2,3,4].scan(:+)
# => [1, 3, 6, 10]

%w[a b c d].scan {|acc, el| acc + '_' + el }
# => ["a", "a_b", "a_b_c", "a_b_c_d"]

正如你所看到的,inject的实现本身是相当优雅的,丑陋的仅仅是因为用一种没有重载的语言实现了重载。