在数组上使用累加器映射

vek*_*sev 5 ruby inject

我在寻找创造一个方法Enumerable,做map,并inject在同一时间。例如,调用它map_with_accumulator

[1,2,3,4].map_with_accumulator(:+)
# => [1, 3, 6, 10]
Run Code Online (Sandbox Code Playgroud)

或用于字符串

['a','b','c','d'].map_with_accumulator {|acc,el| acc + '_' + el}
# => ['a','a_b','a_b_c','a_b_c_d']
Run Code Online (Sandbox Code Playgroud)

我无法找到解决方案。我想我可以用reduce. 我正在沿着以下路径工作:

arr.reduce([]) {|acc,e| ..... }
Run Code Online (Sandbox Code Playgroud)

初始值是一个空数组,但我无法正确理解。

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

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

Jör*_*tag 4

这个操作称为scanprefix_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"]
Run Code Online (Sandbox Code Playgroud)

理想情况下,行为应该与inject4 个重载相匹配(在这种情况下,它将给出您指定的结果),但不幸的是,在 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"]
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,实现本身相当inject优雅,丑陋完全是由于在没有重载的语言中实现了重载。