Подтвердить что ты не робот

Ruby (Rails) # вложение на хэши - хороший стиль?

Внутри Rails-кода люди склонны использовать метод Enumerable # для создания хэшей, например:

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

Хотя это, похоже, стало распространенной идиомой, кто-нибудь видит преимущество над "наивной" версией, которая будет выглядеть следующим образом:

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

Единственное преимущество, которое я вижу для первой версии, заключается в том, что вы делаете это в закрытом блоке, и вы (явно) не инициализируете хэш. В противном случае он злоупотребляет методом неожиданным способом, его труднее понять и усложнить. Так почему это так популярно?

4b9b3361

Ответ 1

Красота находится в глазах смотрящего. Те, у кого какой-то функциональный фон программирования, вероятно, предпочтут метод inject (как и я), потому что он имеет ту же семантику, что и fold порядок заказа, который является обычным способом вычисления одного результата из нескольких входов. Если вы понимаете inject, тогда вы должны понимать, что функция используется по назначению.

Как одна из причин, почему этот подход кажется мне лучше (на мой взгляд), рассмотрим лексический охват переменной hash. В методе inject, hash существует только внутри тела блока. В методе each, переменная hash внутри блока должна соглашаться с некоторым контекстом выполнения, определенным вне блока. Хотите определить другой хеш в одной и той же функции? Используя метод inject, можно вырезать и вставить код inject и использовать его напрямую, и он почти наверняка не будет вводить ошибки (игнорируя, следует ли использовать C & P во время редактирования - люди делают). Используя метод each, вам нужно C & P код и переименуйте переменную hash в любое имя, которое вы хотите использовать, - дополнительный шаг означает, что это более подвержено ошибкам.

Ответ 2

Как указывает Алексей, Hash # update() медленнее, чем Hash # store(), но это заставило меня задуматься об общей эффективности #inject() против прямого цикла #each. Поэтому я сравнивал несколько вещей:

(ПРИМЕЧАНИЕ: Обновлено 19 сентября 2012 года, чтобы включить #each_with_object)

(ПРИМЕЧАНИЕ. Обновлено 31 марта 2014 года, чтобы включить #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 # inject, а хранилище Hash # быстрее, чем обновление Hash #. Но самым быстрым из них является передача массива во время инициализации:

Hash[PAIRS]

Если вы добавляете элементы после создания хэша, выигрышная версия - именно то, что предлагал OP:

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

Но в этом случае, если вы - пурист, который хочет одну лексическую форму, вы можете использовать #tap и #each и получить ту же скорость:

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

Для тех, кто не знаком с краном, он создает привязку приемника (новый хеш) внутри тела и, наконец, возвращает приемник (тот же хеш). Если вы знаете 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

Ответ 3

inject (aka reduce) имеет длительное и уважительное место в языках функционального программирования. Если вы готовы сделать решительный шаг и хотите понять много вдохновений Маца для Ruby, вы должны прочитать семантическую структуру и интерпретацию компьютерных программ, доступных в Интернете по адресу http://mitpress.mit.edu/sicp/.

Некоторые программисты считают стилистически более чистым, чтобы иметь все в одном лексическом пакете. В вашем примере хеширования использование инъекции означает, что вам не нужно создавать пустой хеш в отдельном выражении. Что еще, оператор-инъекция возвращает результат напрямую - вам не нужно помнить, что это в хеш-переменной. Чтобы это было ясно, подумайте:

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

против

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}}

Ответ 4

Я только что нашел в Рубиновый ввод с начальным хешем предложение 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

Ответ 5

Если вы возвращаете хэш, использование слияния может сохранить его более чистым, поэтому вам не нужно возвращать хэш после этого.

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)) }

Ответ 6

Я думаю, что это связано с тем, что люди не понимают, когда использовать сокращение. Я согласен с вами, каждый из них должен быть