在数组上使用累加器映射
Map with accumulator on an array
我想为 Enumerable
创建一个方法,它同时执行 map
和 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
来做。我正在沿着这样的道路前进:
arr.reduce([]) {|acc,e| ..... }
初始值是一个空数组,但我没弄对。
编辑: 请参阅下面 Jörg 的回答以获得正确的解决方案。在阅读他的回答后我意识到另一种(有点粗暴)的方法是使用 instance_eval
,它将给定块的上下文更改为执行它的对象的上下文。所以 self
被设置为引用数组而不是调用上下文(这意味着它不再是闭包!)并且 inject
和 shift
被数组调用。令人费解,不必要的简洁,读起来令人困惑,但它教会了我一些新东西。
['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
的实现本身是相当优雅的,丑陋的仅仅是因为用一种没有重载的语言实现了重载。
我想为 Enumerable
创建一个方法,它同时执行 map
和 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
来做。我正在沿着这样的道路前进:
arr.reduce([]) {|acc,e| ..... }
初始值是一个空数组,但我没弄对。
编辑: 请参阅下面 Jörg 的回答以获得正确的解决方案。在阅读他的回答后我意识到另一种(有点粗暴)的方法是使用 instance_eval
,它将给定块的上下文更改为执行它的对象的上下文。所以 self
被设置为引用数组而不是调用上下文(这意味着它不再是闭包!)并且 inject
和 shift
被数组调用。令人费解,不必要的简洁,读起来令人困惑,但它教会了我一些新东西。
['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
的实现本身是相当优雅的,丑陋的仅仅是因为用一种没有重载的语言实现了重载。