with sort_by method all ruby-on-rails ruby design

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