Ruby(Rails)#inject on hashes - 好风格?

时间:2010-07-12 17:56:05

标签: ruby-on-rails ruby

在Rails代码中,人们倾向于使用Enumerable #injection方法来创建哈希,如下所示:

somme_enum.inject({}) do |hash, element|
  hash[element.foo] = element.bar
  hash
 end

虽然这似乎已经成为一种常见的习惯用法,但是有没有人看到优于“天真”版本的优势,这将是:

hash = {}
some_enum.each { |element| hash[element.foo] = element.bar }

我在第一个版本中看到的唯一优势是你在一个封闭的块中执行它并且你没有(显式地)初始化哈希。否则它会以一种意想不到的方式滥用方法,难以理解并且难以阅读。那为什么它如此受欢迎?

6 个答案:

答案 0 :(得分:30)

正如Aleksey所指出的,Hash#update()比Hash#store()慢,但这让我想到了#inject()与直接#each循环的整体效率。所以我对一些事情进行了基准测试:

(注意:2012年9月19日更新,包括#each_with_object)

(注意:2014年3月31日更新,包括#by_initialization,感谢https://stackoverflow.com/users/244969/pablo的建议)

测试

require 'benchmark'
module HashInject
  extend self

  PAIRS = 1000.times.map {|i| [sprintf("s%05d",i).to_sym, i]}

  def inject_store
    PAIRS.inject({}) {|hash, sym, val| hash[sym] = val ; hash }
  end

  def inject_update
    PAIRS.inject({}) {|hash, sym, val| hash.update(val => hash) }
  end

  def each_store
    hash = {}
    PAIRS.each {|sym, val| hash[sym] = val }
    hash
  end

  def each_update
    hash = {}
    PAIRS.each {|sym, val| hash.update(val => hash) }
    hash
  end

  def each_with_object_store
    PAIRS.each_with_object({}) {|pair, hash| hash[pair[0]] = pair[1]}
  end

  def each_with_object_update
    PAIRS.each_with_object({}) {|pair, hash| hash.update(pair[0] => pair[1])}
  end

  def by_initialization
    Hash[PAIRS]
  end

  def tap_store
    {}.tap {|hash| PAIRS.each {|sym, val| hash[sym] = val}}
  end

  def tap_update
    {}.tap {|hash| PAIRS.each {|sym, val| hash.update(sym => val)}}
  end

  N = 10000

  Benchmark.bmbm do |x|
    x.report("inject_store") { N.times { inject_store }}
    x.report("inject_update") { N.times { inject_update }}
    x.report("each_store") { N.times {each_store }}
    x.report("each_update") { N.times {each_update }}
    x.report("each_with_object_store") { N.times {each_with_object_store }}
    x.report("each_with_object_update") { N.times {each_with_object_update }}
    x.report("by_initialization") { N.times {by_initialization}}
    x.report("tap_store") { N.times {tap_store }}
    x.report("tap_update") { N.times {tap_update }}
  end

end

结果

Rehearsal -----------------------------------------------------------
inject_store             10.510000   0.120000  10.630000 ( 10.659169)
inject_update             8.490000   0.190000   8.680000 (  8.696176)
each_store                4.290000   0.110000   4.400000 (  4.414936)
each_update              12.800000   0.340000  13.140000 ( 13.188187)
each_with_object_store    5.250000   0.110000   5.360000 (  5.369417)
each_with_object_update  13.770000   0.340000  14.110000 ( 14.166009)
by_initialization         3.040000   0.110000   3.150000 (  3.166201)
tap_store                 4.470000   0.110000   4.580000 (  4.594880)
tap_update               12.750000   0.340000  13.090000 ( 13.114379)
------------------------------------------------- total: 77.140000sec

                              user     system      total        real
inject_store             10.540000   0.110000  10.650000 ( 10.674739)
inject_update             8.620000   0.190000   8.810000 (  8.826045)
each_store                4.610000   0.110000   4.720000 (  4.732155)
each_update              12.630000   0.330000  12.960000 ( 13.016104)
each_with_object_store    5.220000   0.110000   5.330000 (  5.338678)
each_with_object_update  13.730000   0.340000  14.070000 ( 14.102297)
by_initialization         3.010000   0.100000   3.110000 (  3.123804)
tap_store                 4.430000   0.110000   4.540000 (  4.552919)
tap_update               12.850000   0.330000  13.180000 ( 13.217637)
=> true

结论

Enumerable#each比Enumerable #inject更快,而Hash #store比Hash #renew更快。但最快的是在初始化时传递一个数组:

Hash[PAIRS]

如果您在创建哈希后添加元素,则获胜版本正是OP建议的那样:

hash = {}
PAIRS.each {|sym, val| hash[sym] = val }
hash

但是在这种情况下,如果你是一个想要单一词汇形式的纯粹主义者,你可以使用#tap和#each并获得相同的速度:

{}.tap {|hash| PAIRS.each {|sym, val| hash[sym] = val}}

对于那些不熟悉tap的人,它会在主体内部创建一个接收器(新哈希)的绑定,最后返回接收者(相同的哈希)。如果您了解Lisp,请将其视为Ruby的LET绑定版本。

-whew-。谢谢你的聆听。

后记

由于人们已经问过,这里是测试环境:

# Ruby version    ruby 2.0.0p247 (2013-06-27) [x86_64-darwin12.4.0]
# OS              Mac OS X 10.9.2
# Processor/RAM   2.6GHz Intel Core i7 / 8GB 1067 MHz DDR3

答案 1 :(得分:22)

美丽是旁观者的眼睛。具有一些函数式编程背景的人可能更喜欢基于inject的方法(就像我一样),因为它具有与fold higher-order function相同的语义,这是从多个计算单个结果的常用方法投入。如果您了解inject,那么您应该了解该功能正在按预期使用。

作为这种方法看起来更好的一个原因(在我看来),请考虑hash变量的词法范围。在基于inject的方法中,hash仅存在于块的主体内。在基于each的方法中,块内的hash变量需要与块外部定义的某些执行上下文一致。想在同一个函数中定义另一个哈希?使用inject方法,可以剪切并粘贴基于inject的代码并直接使用它,并且几乎肯定不会引入错误(忽略是否应该使用C& P)编辑 - 人们这样做。使用each方法,您需要C& P代码,并将hash变量重命名为您想要使用的任何名称 - 额外步骤意味着这更容易出错。

答案 2 :(得分:10)

inject(又名reduce)在函数式编程语言中有着悠久而受人尊敬的地位。如果您准备冒险尝试并希望了解Matz对Ruby的大量启发,那么您应该阅读http://mitpress.mit.edu/sicp/在线提供的开创性的计算机程序结构和解释

一些程序员发现在一个词法包中包含所有内容,风格更清晰。在哈希示例中,使用注入意味着您不必在单独的语句中创建空哈希。更重要的是,inject语句直接返回结果 - 您不必记住它在哈希变量中。为了清楚地说明这一点,请考虑:

[1, 2, 3, 5, 8].inject(:+)

VS

total = 0
[1, 2, 3, 5, 8].each {|x| total += x}

第一个版本返回总和。第二个版本将总和存储在total中,作为程序员,您必须记住使用total而不是.each语句返回的值。

一个小小的附录(纯粹是自然的 - 不是关于注入):你的例子可能写得更好:

some_enum.inject({}) {|hash, element| hash.update(element.foo => element.bar) }

...因为hash.update()返回哈希本身,所以最后不需要额外的hash语句。

更新

@Aleksey让我感到羞愧,因为他对各种组合进行了基准测试。请参阅我在其他地方的基准回复。简短形式:

hash = {}
some_enum.each {|x| hash[x.foo] = x.bar}
hash 

是最快的,但可以稍微优雅地重铸 - 而且速度一样快: -

{}.tap {|hash| some_enum.each {|x| hash[x.foo] = x.bar}}

答案 3 :(得分:3)

我刚刚找到了 Ruby inject with initial being a hash 建议使用each_with_object代替inject

hash = some_enum.each_with_object({}) do |element, h|
  h[element.foo] = element.bar
end

对我来说似乎很自然。

另一种方法,使用tap

hash = {}.tap do |h|
  some_enum.each do |element|
    h[element.foo] = element.bar
  end
end

答案 4 :(得分:2)

如果你要返回一个哈希,使用merge可以保持它更干净,所以你不必在之后返回哈希。

some_enum.inject({}){|h,e| h.merge(e.foo => e.bar) }

如果你的枚举是一个哈希,你可以很好地得到键和值(k,v)。

some_hash.inject({}){|h,(k,v)| h.merge(k => do_something(v)) }

答案 5 :(得分:1)

我认为这与不完全了解何时使用reduce的人有关。我同意你的观点,每一种方式都应该是