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

ave*_*ell 27 ruby ruby-on-rails

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

somme_enum.inject({}) do |hash, element|
  hash[element.foo] = element.bar
  hash
 end
Run Code Online (Sandbox Code Playgroud)

虽然这似乎已成为一种常见的习语,但是有没有人看到优于"天真"版本的优势,这将是:

hash = {}
some_enum.each { |element| hash[element.foo] = element.bar }
Run Code Online (Sandbox Code Playgroud)

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

fea*_*ool 30

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

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

(注意:2014年3月31日更新,包括#by_initialization,感谢/sf/users/17147861/的建议 )

测试

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
Run Code Online (Sandbox Code Playgroud)

结果

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
Run Code Online (Sandbox Code Playgroud)

结论

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

Hash[PAIRS]
Run Code Online (Sandbox Code Playgroud)

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

hash = {}
PAIRS.each {|sym, val| hash[sym] = val }
hash
Run Code Online (Sandbox Code Playgroud)

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

{}.tap {|hash| PAIRS.each {|sym, val| hash[sym] = val}}
Run Code Online (Sandbox Code Playgroud)

对于不熟悉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
Run Code Online (Sandbox Code Playgroud)

  • 我知道这已经有几年了,但我想我会指出,在你的“insert_update”和“each_update”中,你将值设置为散列而不是对中的值,我希望这会有所不同为基准。这可能是一个可以忽略不计的差异,但为了一致性,可能值得修复并重新运行基准测试。看看 Ruby 的新版本如何进行相同的测试也很有趣,但这不是我评论的重点! (2认同)

Aid*_*lly 23

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

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

  • 注入方法IMO唯一的美学问题是需要从块中显式返回哈希,因为`Hash#[] =`方法返回分配的值,而不是哈希本身.我不知道为什么他们省略了另一种方法就可以做到这一点. (2认同)

fea*_*ool 10

inject(aka reduce)在函数式编程语言中有着悠久而受人尊敬的地方.如果你准备花了一大笔钱,想明白了很多的Matz的灵感为Ruby的,你应该阅读开创性的计算机程序的结构与解释,可在网上http://mitpress.mit.edu/sicp/.

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

[1, 2, 3, 5, 8].inject(:+)
Run Code Online (Sandbox Code Playgroud)

VS

total = 0
[1, 2, 3, 5, 8].each {|x| total += x}
Run Code Online (Sandbox Code Playgroud)

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

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

some_enum.inject({}) {|hash, element| hash.update(element.foo => element.bar) }
Run Code Online (Sandbox Code Playgroud)

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

更新

@Aleksey让我陷入了各种组合的基准测试.请参阅我在其他地方的基准回复.简写:

hash = {}
some_enum.each {|x| hash[x.foo] = x.bar}
hash 
Run Code Online (Sandbox Code Playgroud)

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

{}.tap {|hash| some_enum.each {|x| hash[x.foo] = x.bar}}
Run Code Online (Sandbox Code Playgroud)