ruby on rails - sort_by - Ruby(Rails)#inject en hashes-¿buen estilo?
ruby sort_by (6)
Dentro del código de Rails, las personas tienden a usar el método de Inumerable # Inject para crear hash, como este:
somme_enum.inject({}) do |hash, element|
hash[element.foo] = element.bar
hash
end
Si bien esto parece haberse convertido en una expresión común, ¿alguien ve una ventaja sobre la versión "ingenua", que sería como:
hash = {}
some_enum.each { |element| hash[element.foo] = element.bar }
La única ventaja que veo para la primera versión es que lo haces en un bloque cerrado y no (explícitamente) inicializas el hash. De lo contrario, abusa de un método de forma inesperada, es más difícil de entender y más difícil de leer. Entonces ¿por qué es tan popular?
Acabo de encontrar en Ruby inject con una sugerencia de hash inicial para usar each_with_object
lugar de each_with_object
:
hash = some_enum.each_with_object({}) do |element, h|
h[element.foo] = element.bar
end
Me parece natural.
Otra forma, usando el tap
:
hash = {}.tap do |h|
some_enum.each do |element|
h[element.foo] = element.bar
end
end
Como señala Aleksey, Hash # update () es más lento que Hash # store (), pero eso me hizo pensar en la eficiencia general de #inject () frente a un # sencillo bucle. Así que hice una evaluación comparativa de algunas cosas:
(NOTA: actualizado el 19 de septiembre de 2012 para incluir #each_with_object)
(NOTA: actualizado el 31 de marzo de 2014 para incluir #by_initialization, gracias a la sugerencia de https://.com/users/244969/pablo )
los exámenes
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
Los resultados
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
conclusión
Enumerable # cada uno es más rápido que Enumerable # inject, y Hash # store es más rápido que Hash # update. Pero el más rápido de todos es pasar una matriz en el momento de la inicialización:
Hash[PAIRS]
Si está agregando elementos después de que se ha creado el hash, la versión ganadora es exactamente lo que el OP sugería:
hash = {}
PAIRS.each {|sym, val| hash[sym] = val }
hash
Pero en ese caso, si eres un purista que quiere una sola forma léxica, puedes usar #tap y #each y obtener la misma velocidad:
{}.tap {|hash| PAIRS.each {|sym, val| hash[sym] = val}}
Para aquellos que no están familiarizados con el tap, crea un enlace del receptor (el nuevo hash) dentro del cuerpo, y finalmente devuelve el receptor (el mismo hash). Si conoces a Lisp, piensa que es la versión de Ruby del enlace LET.
-Uf-. Gracias por su atención.
posdata
Como la gente ha preguntado, este es el entorno de prueba:
# 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
Creo que tiene que ver con personas que no entienden completamente cuándo usar reducir. Estoy de acuerdo contigo, cada una es la forma en que debería ser
La belleza está en el ojo del espectador. Aquellos con algún fondo de programación funcional probablemente preferirán el método basado en inject
(como yo), porque tiene la misma semántica que la función de orden superior , que es una forma común de calcular un único resultado de múltiples entradas. Si entiende la inject
, debe comprender que la función se está utilizando como se pretendía.
Como una razón por la cual este enfoque parece mejor (a mis ojos), considere el alcance léxico de la variable hash
. En el método basado en inject
, hash
solo existe dentro del cuerpo del bloque. En el método basado en each
, la variable hash
dentro del bloque necesita estar de acuerdo con algún contexto de ejecución definido fuera del bloque. ¿Quieres definir otro hash en la misma función? Usando el método de inject
, es posible cortar y pegar el código basado en la inject
y usarlo directamente, y es casi seguro que no introduzca errores (ignorando si uno debería usar C & P durante la edición, la gente sí). Utilizando each
método, necesita C & P el código y cambiar el nombre de la variable hash
al nombre que quiera usar; el paso adicional significa que es más propenso a error.
Si devuelve un hash, usar fusionar puede mantenerlo más limpio para que no tenga que devolver el hash después.
some_enum.inject({}){|h,e| h.merge(e.foo => e.bar) }
Si su enumeración es un hash, puede obtener la clave y valorarla bien con (k, v).
some_hash.inject({}){|h,(k,v)| h.merge(k => do_something(v)) }
inject
(aka reduce
) tiene un lugar largo y respetado en los lenguajes de programación funcionales. Si estás listo para dar el paso y quieres entender mucha de la inspiración de Matz para Ruby, deberías leer la Estructura e Interpretación de los Programas de Computadora , disponible en línea en http://mitpress.mit.edu/sicp/ .
Algunos programadores encuentran estilísticamente más limpio tener todo en un paquete léxico. En su ejemplo de hash, al usar Inject significa que no tiene que crear un hash vacío en una declaración separada. Además, la instrucción de inyección devuelve el resultado directamente; no tiene que recordar que está en la variable hash. Para dejarlo realmente claro, considera:
[1, 2, 3, 5, 8].inject(:+)
vs
total = 0
[1, 2, 3, 5, 8].each {|x| total += x}
La primera versión devuelve la suma. La segunda versión almacena la suma en total
, y como programador, debe recordar usar total
lugar del valor devuelto por la declaración .each
.
Una pequeña adición (y puramente idónea, no sobre inyección): su ejemplo podría estar mejor escrito:
some_enum.inject({}) {|hash, element| hash.update(element.foo => element.bar) }
... ya que hash.update()
devuelve el hash en sí mismo, no necesita la declaración adicional de hash
al final.
actualizar
@Aleksey me ha avergonzado de hacer una evaluación comparativa de las diversas combinaciones. Ver mi respuesta de evaluación comparativa en otro lugar aquí. Forma corta:
hash = {}
some_enum.each {|x| hash[x.foo] = x.bar}
hash
es el más rápido, pero puede ser refundido un poco más elegantemente, y es tan rápido como:
{}.tap {|hash| some_enum.each {|x| hash[x.foo] = x.bar}}