En Ruby, ¿cómo hago un hash a partir de una matriz?

76

Tengo una matriz simple:

arr = ["apples", "bananas", "coconuts", "watermelons"]

También tengo una función fque realizará una operación en una sola entrada de cadena y devolverá un valor. Esta operación es muy cara, por lo que me gustaría memorizar los resultados en el hash.

Sé que puedo hacer el hash deseado con algo como esto:

h = {}
arr.each { |a| h[a] = f(a) }

Lo que me gustaría hacer es no tener que inicializar h, para poder escribir algo como esto:

h = arr.(???) { |a| a => f(a) }

¿Se puede hacer eso?

Wizzlewott
fuente

Respuestas:

128

Supongamos que tiene una función con un nombre funcional: "f"

def f(fruit)
   fruit + "!"
end

arr = ["apples", "bananas", "coconuts", "watermelons"]
h = Hash[ *arr.collect { |v| [ v, f(v) ] }.flatten ]

Te regalaré:

{"watermelons"=>"watermelons!", "bananas"=>"bananas!", "apples"=>"apples!", "coconuts"=>"coconuts!"}

Actualizado:

Como se menciona en los comentarios, Ruby 1.8.7 introduce una sintaxis más agradable para esto:

h = Hash[arr.collect { |v| [v, f(v)] }]
microspino
fuente
Creo que quisiste decir ... { |v| [v, f(v)] }, ¡pero esto funcionó!
Wizzlewott
3
Solo una cosa: ¿por qué hay una al *lado *arr.collect?
Jeriko
3
@Jeriko: el operador splat *recopila una lista en una matriz o desenrolla una matriz en una lista, según el contexto. Aquí desenrolla la matriz en una lista (que se utilizará como elementos para el nuevo hash).
Telemachus
2
Después de mirar la respuesta de Jörg y pensando en esto durante un poco más, nota que se puede eliminar tanto *y flattenpara una versión más simple: h = Hash[ arr.collect { |v| [ v, f(v) ] } ]. Sin embargo, no estoy seguro de si hay un problema que no veo.
Telemachus
3
En Ruby 1.8.7, lo feo Hash[*key_pairs.flatten]es simplemente Hash[key_pairs]. Mucho mejor, y require 'backports'si aún no ha actualizado desde 1.8.6.
Marc-André Lafortune
56

Hice algunos puntos de referencia rápidos y sucios en algunas de las respuestas dadas. (Es posible que estos hallazgos no sean exactamente idénticos a los suyos según la versión de Ruby, almacenamiento en caché extraño, etc., pero los resultados generales serán similares).

arr es una colección de objetos ActiveRecord.

Benchmark.measure {
    100000.times {
        Hash[arr.map{ |a| [a.id, a] }]
    }
}

Benchmark @ real = 0.860651, @ cstime = 0.0, @ cutime = 0.0, @ stime = 0.0, @ utime = 0.8500000000000005, @ total = 0.8500000000000005

Benchmark.measure { 
    100000.times {
        h = Hash[arr.collect { |v| [v.id, v] }]
    }
}

Benchmark @ real = 0.74612, @ cstime = 0.0, @ cutime = 0.0, @ stime = 0.010000000000000009, @ utime = 0.740000000000002, @ total = 0.750000000000002

Benchmark.measure {
    100000.times {
        hash = {}
        arr.each { |a| hash[a.id] = a }
    }
}

Benchmark @ real = 0.627355, @ cstime = 0.0, @ cutime = 0.0, @ stime = 0.010000000000000009, @ utime = 0.6199999999999974, @ total = 0.6299999999999975

Benchmark.measure {
    100000.times {
        arr.each_with_object({}) { |v, h| h[v.id] = v }
    }
}

Benchmark @ real = 1.650568, @ cstime = 0.0, @ cutime = 0.0, @ stime = 0.12999999999999998, @ utime = 1.51, @ total = 1.64

En conclusión

El hecho de que Ruby sea expresiva y dinámica no significa que siempre debas buscar la solución más bonita. El básico de cada ciclo fue el más rápido en la creación de un hash.

dmastylo
fuente
7
Tú, amigo mío, eres increíble por hacer tu tarea y publicarla :)
Alexander Bird
Ligeramente más rápido de usar una variable de bucle incrementada manualmente: no tengo su conjunto de datos, acabo de cocinar un objeto trivial con un acceso @id y más o menos coincidí con sus números, pero la iteración directa redujo un par de%. Estilísticamente, prefiero {} .tap {| h | ....} para asignar un hash, porque me gustan los fragmentos encapsulados.
android.weasel
32
h = arr.each_with_object({}) { |v,h| h[v] = f(v) }
megas
fuente
2
Esto es mucho más conciso que usar Hash [arr.collect {...}]
kaichanvong
1
Esto es increíblemente lento,
mira
11

Esto es lo que probablemente escribiría:

h = Hash[arr.zip(arr.map(&method(:f)))]

Simple, claro, obvio, declarativo. ¿Qué más podrías querer?

Jörg W Mittag
fuente
1
Me gusta ziptanto como el próximo chico, pero como ya estamos llamando map, ¿por qué no dejarlo así? h = Hash[ arr.map { |v| [ v, f(v) ] } ]¿Hay alguna ventaja en tu versión que no veo?
Telemachus
@Telemachus: Con todo el código Haskell que he estado leyendo, me acabo de acostumbrar a la programación sin puntos, eso es todo.
Jörg W Mittag
5

Lo estoy haciendo como se describe en este gran artículo http://robots.thoughtbot.com/iteration-as-an-anti-pattern#build-a-hash-from-an-array

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h.merge(fruit => f(fruit)) }

Más información sobre el injectmétodo: http://ruby-doc.org/core-2.0.0/Enumerable.html#method-i-inject

Vlado Cingel
fuente
Esto hace una mergepara cada paso de la iteración. Fusionar es O (n), al igual que la iteración. Por lo tanto, esto es O(n^2)mientras que el problema en sí es obviamente lineal. En términos absolutos, solo probé esto en una matriz con 100k elementos y tomó 730 seconds, mientras que los otros métodos mencionados en este hilo tomaron desde 0.7hasta 1.1 seconds. ¡Sí, eso es una desaceleración por Factor 700 !
Matthias Winkelmann
1

Otro, un poco más claro en mi humilde opinión:

Hash[*array.reduce([]) { |memo, fruit| memo << fruit << f(fruit) }]

Usando longitud como f () -

2.1.5 :026 > array = ["apples", "bananas", "coconuts", "watermelons"]
 => ["apples", "bananas", "coconuts", "watermelons"] 
2.1.5 :027 > Hash[*array.reduce([]) { |memo, fruit| memo << fruit << fruit.length }]
 => {"apples"=>6, "bananas"=>7, "coconuts"=>8, "watermelons"=>11} 
2.1.5 :028 >
Shreyas
fuente
1

además de la respuesta de Vlado Cingel (todavía no puedo agregar un comentario, así que agregué una respuesta).

Inject también se puede utilizar de esta manera: el bloque debe devolver el acumulador. Solo la asignación en el bloque devuelve el valor de la asignación y se informa de un error.

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h[fruit]= f(fruit); h }
rudo
fuente
Evalué las dos versiones: el uso de la combinación duplica el tiempo de ejecución. La versión de inyección anterior es una comparación con la versión de recopilación de microspino
ruud