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 }
Единственное преимущество, которое я вижу для первой версии, заключается в том, что вы делаете это в закрытом блоке, и вы (явно) не инициализируете хэш. В противном случае он злоупотребляет методом неожиданным способом, его труднее понять и усложнить. Так почему это так популярно?
Ответы
Ответ 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
Я думаю, что это связано с тем, что люди не понимают, когда использовать сокращение. Я согласен с вами, каждый из них должен быть