在数组上映射累加器

时间:2015-06-23 02:37:28

标签: ruby inject

我希望为同时执行Enumerablemap的{​​{1}}创建一种方法。例如,将其称为inject

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

初始值为空数组,但我无法正确显示。

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

shift

3 个答案:

答案 0 :(得分:4)

此操作称为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本身的实现相当优雅,丑陋仅仅是因为在没有超载的情况下实现了语言的重载。

答案 1 :(得分:3)

你可以这样做:

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] 

答案 2 :(得分:2)

以下是使用reduce

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