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?
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?
Respuestas:
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.
[nil, nil, nil].comprehend {|x| x }
que regresa []
.
compact!
devuelve nil en lugar de la matriz cuando no se cambia ningún elemento, así que no creo que eso funcione.
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 ...
some_array.map{|x| x * 3 unless x % 2}.compact
, que posiblemente sea más legible / ruby-esque.
unless x%2
no tiene ningún efecto ya que 0 es verdadero en ruby. Ver: gist.github.com/jfarmer/2647362
Hice un benchmark rápido comparando las tres alternativas y map-compact realmente parece ser la mejor opción.
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
/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
reduce
en este punto de referencia (consulte stackoverflow.com/a/17703276 ).
inject
==reduce
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.
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#select
también sea muy buena.
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}
2
cosas n
veces en lugar de 1
cosas n
veces y luego otra 1
cosa n
veces :) Una ventaja importante de inject
/ reduce
es que conserva cualquier nil
valor en la secuencia de entrada, que es un comportamiento más comprensivo de listas
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.
Enumerable tiene un grep
mé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.map
o 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 nil
usa en las compact
sugerencias -using.
Esto es más conciso:
[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
[1,2,3,4,5,6].select(&:even?).map(&3.method(:*))
Como mencionó Pedro, puede fusionar las llamadas encadenadas a Enumerable#select
y Enumerable#map
, evitando un cruce sobre los elementos seleccionados. Esto es cierto porque Enumerable#select
es 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 comprehend
implementación de Robert Gamble para hacer que este select
/ map
patrón sea más bonito.
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]
lazy
en Array y luego?(1..6).lazy{|x|x*3 if x.even?}
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]
Se introdujo Ruby 2.7, filter_map
que 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í .
https://rubygems.org/gems/ruby_list_comprehension
enchufe desvergonzado para mi gema de comprensión de la lista Ruby para permitir comprensiones idiomáticas de la lista Ruby
$l[for x in 1..10 do x + 2 end] #=> [3, 4, 5 ...]
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 select
mé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 map
o en su collect
lugar.