Comprensión de listas en Ruby

93

Para hacer el equivalente a las comprensiones de listas de Python, estoy haciendo lo siguiente:

some_array.select{|x| x % 2 == 0 }.collect{|x| x * 3}

¿Hay una mejor manera de hacer esto ... quizás con una llamada a un método?

Solo lectura
fuente
3
Tanto las respuestas tuyas como las de glenn mcdonald me parecen bien ... No veo lo que ganarías si tratas de ser más conciso que ninguno de los dos.
Pistos
1
esta solución atraviesa la lista dos veces. La inyección no lo hace.
Pedro Rolo
2
Algunas respuestas increíbles aquí, pero también sería increíble ver ideas para la comprensión de listas en múltiples colecciones.
Bo Jeanes

Respuestas:

55

Si realmente lo desea, puede crear un método de comprensión Array # como este:

class Array
  def comprehend(&block)
    return self if block.nil?
    self.collect(&block).compact
  end
end

some_array = [1, 2, 3, 4, 5, 6]
new_array = some_array.comprehend {|x| x * 3 if x % 2 == 0}
puts new_array

Huellas dactilares:

6
12
18

Probablemente lo haría de la forma en que lo hizo.

Robert Gamble
fuente
2
¡Podrías usar compacto! para optimizar un poco
Alexey
9
Esto no es realmente correcto, considere: [nil, nil, nil].comprehend {|x| x }que regresa [].
Ted Kaplan
alexey, de acuerdo con los documentos, compact!devuelve nil en lugar de la matriz cuando no se cambia ningún elemento, así que no creo que eso funcione.
Archivo binario
89

Que tal si:

some_array.map {|x| x % 2 == 0 ? x * 3 : nil}.compact

Ligeramente más limpio, al menos para mi gusto, y según una prueba de referencia rápida, aproximadamente un 15% más rápido que su versión ...

glenn mcdonald
fuente
4
así como some_array.map{|x| x * 3 unless x % 2}.compact, que posiblemente sea más legible / ruby-esque.
Nightpool
5
@nightpool unless x%2no tiene ningún efecto ya que 0 es verdadero en ruby. Ver: gist.github.com/jfarmer/2647362
Abhinav Srivastava
30

Hice un benchmark rápido comparando las tres alternativas y map-compact realmente parece ser la mejor opción.

Prueba de rendimiento (rieles)

require 'test_helper'
require 'performance_test_help'

class ListComprehensionTest < ActionController::PerformanceTest

  TEST_ARRAY = (1..100).to_a

  def test_map_compact
    1000.times do
      TEST_ARRAY.map{|x| x % 2 == 0 ? x * 3 : nil}.compact
    end
  end

  def test_select_map
    1000.times do
      TEST_ARRAY.select{|x| x % 2 == 0 }.map{|x| x * 3}
    end
  end

  def test_inject
    1000.times do
      TEST_ARRAY.inject([]) {|all, x| all << x*3 if x % 2 == 0; all }
    end
  end

end

Resultados

/usr/bin/ruby1.8 -I"lib:test" "/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/performance/list_comprehension_test.rb" -- --benchmark
Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
ListComprehensionTest#test_inject (1230 ms warmup)
           wall_time: 1221 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.ListComprehensionTest#test_map_compact (860 ms warmup)
           wall_time: 855 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.ListComprehensionTest#test_select_map (961 ms warmup)
           wall_time: 955 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.
Finished in 66.683039 seconds.

15 tests, 0 assertions, 0 failures, 0 errors
knuton
fuente
1
También sería interesante verlo reduceen este punto de referencia (consulte stackoverflow.com/a/17703276 ).
Adam Lindberg
3
inject==reduce
ben.snape
map_compact quizás más rápido, pero está creando una nueva matriz. Inject es más eficiente en el espacio que map.compact y select.map
bibstha
11

Parece haber cierta confusión entre los programadores de Ruby en este hilo sobre qué es la comprensión de listas. Cada respuesta asume alguna matriz preexistente para transformar. Pero el poder de la comprensión de listas radica en una matriz creada sobre la marcha con la siguiente sintaxis:

squares = [x**2 for x in range(10)]

Lo siguiente sería un análogo en Ruby (la única respuesta adecuada en este hilo, AFAIC):

a = Array.new(4).map{rand(2**49..2**50)} 

En el caso anterior, estoy creando una matriz de enteros aleatorios, pero el bloque podría contener cualquier cosa. Pero esta sería una lista de comprensión de Ruby.

marca
fuente
1
¿Cómo harías lo que el OP está tratando de hacer?
Andrew Grimm
2
En realidad, veo ahora que el propio OP tenía una lista existente que el autor quería transformar. Pero la concepción arquetípica de la comprensión de listas implica la creación de una matriz / lista donde no existía antes haciendo referencia a alguna iteración. Pero en realidad, algunas definiciones formales dicen que la comprensión de listas no puede usar el mapa en absoluto, por lo que incluso mi versión no es kosher, pero supongo que es lo más cercana que se podría obtener en Ruby.
Mark
5
No entiendo cómo se supone que su ejemplo de Ruby es un análogo de su ejemplo de Python. El código Ruby debería leer: cuadrados = (0..9) .map {| x | x ** 2}
michau
4
Si bien @michau tiene razón, el objetivo de la comprensión de la lista (que Mark descuidó) es que la comprensión de la lista en sí misma no usa, no genera arreglos, usa generadores y co rutinas para hacer todos los cálculos en forma de transmisión sin asignar almacenamiento en absoluto (excepto temp variables) hasta que (iff) los resultados lleguen a una variable de matriz; este es el propósito de los corchetes en el ejemplo de Python, colapsar la comprensión en un conjunto de resultados. Ruby no tiene una instalación similar a los generadores.
Guss
4
Oh, sí, tiene (desde Ruby 2.0): squares_of_all_natural_numbers = (0..Float :: INFINITY) .lazy.map {| x | x ** 2}; p squares_of_all_natural_numbers.take (10) .to_a
michau
11

Discutí este tema con Rein Henrichs, quien me dice que la solución de mejor rendimiento es

map { ... }.compact

Esto tiene sentido porque evita la construcción de matrices intermedias como ocurre con el uso inmutable de Enumerable#inject y evita el crecimiento de la matriz, lo que provoca la asignación. Es tan general como cualquiera de los demás, a menos que su colección pueda contener elementos nulos.

No he comparado esto con

select {...}.map{...}

Es posible que la implementación de Ruby en C Enumerable#selecttambién sea muy buena.

jvoorhis
fuente
9

Una solución alternativa que funcionará en todas las implementaciones y se ejecutará en tiempo O (n) en lugar de O (2n) es:

some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}
Pedro Rolo
fuente
11
Quiere decir que recorre la lista solo una vez. Si sigue la definición formal, O (n) es igual a O (2n). Simplemente quisquilloso :)
Daniel Hepper
1
@Daniel Harper :) No solo tienes razón, sino también para el caso medio, atravesar la lista una vez para descartar algunas entradas y luego volver a realizar una operación puede ser realmente mejor en los casos medios :)
Pedro Rolo
En otras palabras, estás haciendo 2cosas nveces en lugar de 1cosas nveces y luego otra 1cosa nveces :) Una ventaja importante de inject/ reducees que conserva cualquier nilvalor en la secuencia de entrada, que es un comportamiento más comprensivo de listas
John La Rooy
8

Acabo de publicar la gema de comprensión en RubyGems, que te permite hacer esto:

require 'comprehend'

some_array.comprehend{ |x| x * 3 if x % 2 == 0 }

Está escrito en C; la matriz solo se atraviesa una vez.

histocrat
fuente
7

Enumerable tiene un grepmétodo cuyo primer argumento puede ser un predicado proc, y cuyo segundo argumento opcional es una función de mapeo; entonces lo siguiente funciona:

some_array.grep(proc {|x| x % 2 == 0}) {|x| x*3}

Esto no es tan legible como un par de otras sugerencias (me gusta la gema de comprensión simple select.mapo histocrat de anoiaque), pero sus puntos fuertes son que ya es parte de la biblioteca estándar, es de un solo paso y no implica la creación de matrices intermedias temporales. , y no requiere un valor fuera de límites como el que se nilusa en las compactsugerencias -using.

Peter Moulder
fuente
4

Esto es más conciso:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
anoiaque
fuente
2
O, para aún más genialidad sin puntos[1,2,3,4,5,6].select(&:even?).map(&3.method(:*))
Jörg W Mittag
4
[1, 2, 3, 4, 5, 6].collect{|x| x * 3 if x % 2 == 0}.compact
=> [6, 12, 18]

Funciona para mi. También está limpio. Sí, es lo mismo que map, pero creo que collecthace que el código sea más comprensible.


select(&:even?).map()

en realidad se ve mejor, después de verlo a continuación.

Vince
fuente
2

Como mencionó Pedro, puede fusionar las llamadas encadenadas a Enumerable#selecty Enumerable#map, evitando un cruce sobre los elementos seleccionados. Esto es cierto porque Enumerable#selectes una especialización de fold o inject. Publiqué una introducción apresurada al tema en el subreddit de Ruby.

Fusionar manualmente las transformaciones de Array puede ser tedioso, por lo que tal vez alguien pueda jugar con la comprehendimplementación de Robert Gamble para hacer que este select/ mappatrón sea más bonito.

jvoorhis
fuente
2

Algo como esto:

def lazy(collection, &blk)
   collection.map{|x| blk.call(x)}.compact
end

Llámalo:

lazy (1..6){|x| x * 3 if x.even?}

Que devuelve:

=> [6, 12, 18]
Alexandre Magro
fuente
¿Qué hay de malo en definir lazyen Array y luego?(1..6).lazy{|x|x*3 if x.even?}
Guss
1

Otra solución pero quizás no la mejor

some_array.flat_map {|x| x % 2 == 0 ? [x * 3] : [] }

o

some_array.each_with_object([]) {|x, list| x % 2 == 0 ? list.push(x * 3) : nil }
joegiralt
fuente
0

Esta es una forma de abordar esto:

c = -> x do $*.clear             
  if x['if'] && x[0] != 'f' .  
    y = x[0...x.index('for')]    
    x = x[x.index('for')..-1]
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  elsif x['if'] && x[0] == 'f'
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << x")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  elsif !x['if'] && x[0] != 'f'
    y = x[0...x.index('for')]
    x = x[x.index('for')..-1]
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  else
    eval(x.split[3]).to_a
  end
end 

así que básicamente estamos convirtiendo una cadena a la sintaxis ruby ​​adecuada para el bucle, luego podemos usar la sintaxis de Python en una cadena para hacer:

c['for x in 1..10']
c['for x in 1..10 if x.even?']
c['x**2 for x in 1..10 if x.even?']
c['x**2 for x in 1..10']

# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# [2, 4, 6, 8, 10]
# [4, 16, 36, 64, 100]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

o si no le gusta el aspecto de la cadena o tener que usar una lambda, podríamos renunciar al intento de reflejar la sintaxis de Python y hacer algo como esto:

S = [for x in 0...9 do $* << x*2 if x.even? end, $*][1]
# [0, 4, 8, 12, 16]
Sam Michael
fuente
0

Se introdujo Ruby 2.7, filter_mapque prácticamente logra lo que desea (mapa + compacto):

some_array.filter_map { |x| x * 3 if x % 2 == 0 }

Puedes leer más sobre esto aquí .

Matheus Richard
fuente
-4

Creo que la más lista de comprensión sería la siguiente:

some_array.select{ |x| x * 3 if x % 2 == 0 }

Dado que Ruby nos permite colocar el condicional después de la expresión, obtenemos una sintaxis similar a la versión Python de la lista de comprensión. Además, dado que el selectmétodo no incluye nada que equivalga a false, todos los valores nulos se eliminan de la lista resultante y no es necesaria ninguna llamada a compact, como sería el caso si hubiéramos usado mapo en su collectlugar.

Christopher Roach
fuente
7
Esto no parece funcionar. Al menos en Ruby 1.8.6, [1,2,3,4,5,6] .select {| x | x * 3 si x% 2 == 0} se evalúa como [2, 4, 6] Enumerable # select solo se preocupa por si el bloque se evalúa como verdadero o falso, no por el valor que genera, AFAIK.
Greg Campbell