Ruby (Rails) #inject для хэшей - хороший стиль? - PullRequest
27 голосов
/ 12 июля 2010

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

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

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

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

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

Ответы [ 6 ]

30 голосов
/ 12 июля 2012

Как отмечает Алексей, 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 # каждый быстрее, чем Enumerable # inject, а Hash # store быстрее, чем Hash # update. Но быстрее всего передать массив во время инициализации:

Hash[PAIRS]

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

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
23 голосов
/ 12 июля 2010

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

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

10 голосов
/ 10 мая 2011

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

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

[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 в конце.

обновление

@ Алексей посрамил меня на сравнительный анализ различных комбинаций. Смотрите мой ответ по тестированию в другом месте здесь. Сокращенная форма:

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

- самый быстрый, но может быть изменен немного более элегантно - и так же быстро - как:

{}.tap {|hash| some_enum.each {|x| hash[x.foo] = x.bar}}
3 голосов
/ 08 июля 2012

Я только что нашел в Ruby-инъекцию с начальным хешем предложение использовать 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
2 голосов
/ 14 июня 2013

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

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)) }
1 голос
/ 12 июля 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...