En Ruby, ¿existe un método Array que combine 'seleccionar' y 'mapa'?

95

Tengo una matriz de Ruby que contiene algunos valores de cadena. Necesito:

  1. Encuentra todos los elementos que coinciden con algún predicado
  2. Ejecute los elementos coincidentes a través de una transformación
  3. Devuelve los resultados como una matriz

En este momento, mi solución se ve así:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

¿Existe un método Array o Enumerable que combine select y map en una sola declaración lógica?

Seth Petry-Johnson
fuente
5
No hay un método en este momento, pero una propuesta para agregar uno a Ruby: bugs.ruby-lang.org/issues/5663
stefankolb
El Enumerable#grepmétodo hace exactamente lo que se le pidió y ha estado en Ruby durante más de diez años. Requiere un argumento predicado y un bloque de transformación. @hirolau da la única respuesta correcta a esta pregunta.
inopinatus
2
Ruby 2.7 se presenta filter_mapcon este propósito exacto. Más info aquí .
SRack el

Respuestas:

114

Normalmente utilizo mapy compactjunto con mis criterios de selección como sufijo if. compactse deshace de los nulos.

jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}    
 => [3, 3, 3, nil, nil, nil] 


jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}.compact
 => [3, 3, 3] 
Jed Schneider
fuente
1
Ah-ha, estaba tratando de averiguar cómo ignorar los nulos devueltos por mi bloque de mapa. ¡Gracias!
Seth Petry-Johnson
No hay problema, me encanta el compacto. se sienta discretamente y hace su trabajo. También prefiero este método a encadenar funciones enumerables para criterios de selección simples porque es muy declarativo.
Jed Schneider
4
No estaba seguro de si map+ compactrealmente funcionaría mejor que injecty publiqué mis resultados de referencia en un hilo relacionado: stackoverflow.com/questions/310426/list-comprehension-in-ruby/…
knuton
3
esto eliminará todos los nulos, tanto los nulos originales como los que no cumplen con sus criterios. Así que ten cuidado
user1143669
1
No elimina por completo el encadenamiento mapy select, es solo que compactes un caso especial rejectque funciona con cero y funciona algo mejor debido a que se implementó directamente en C.
Joe Atzberger
53

Puede usar reducepara esto, que requiere solo un pase:

[1,1,1,2,3,4].reduce([]) { |a, n| a.push(n*3) if n==1; a }
=> [3, 3, 3] 

En otras palabras, inicialice el estado para que sea el que desee (en nuestro caso, una lista vacía para llenar :) [], luego asegúrese siempre de devolver este valor con modificaciones para cada elemento en la lista original (en nuestro caso, el elemento modificado empujado a la lista).

Este es el más eficiente ya que solo recorre la lista con un pase ( map+ selecto compactrequiere dos pases).

En tu caso:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end
Adam Lindberg
fuente
20
¿No each_with_objecttiene más sentido? No es necesario que devuelva la matriz al final de cada iteración del bloque. Simplemente puede hacerlo my_array.each_with_object([]) { |i, a| a << i if i.condition }.
henrebotha
@henrebotha Quizás sí. Vengo de un entorno funcional, por eso encontré reduceprimero 😊
Adam Lindberg
33

Ruby 2.7+

¡Hay ahora!

Ruby 2.7 se presenta filter_mapcon este propósito exacto. Es idiomático y performativo, y espero que se convierta en la norma muy pronto.

Por ejemplo:

numbers = [1, 2, 5, 8, 10, 13]
enum.filter_map { |i| i * 2 if i.even? }
# => [4, 16, 20]

He aquí una buena lectura sobre el tema .

¡Espero que sea útil para alguien!

SRack
fuente
1
No importa la frecuencia con la que actualice, siempre hay una característica interesante en la próxima versión.
mlt
Agradable. Un problema podría ser que filter, dado que , selecty find_allson sinónimos, tal mapy como collectson, puede resultar difícil recordar el nombre de este método. Es que filter_map, select_collect, find_all_mapo filter_collect?
Eric Duminil
19

Otra forma diferente de abordar esto es usando el nuevo (en relación con esta pregunta) Enumerator::Lazy:

def example
  @lines.lazy
        .select { |line| line.property == requirement }
        .map    { |line| transforming_method(line) }
        .uniq
        .sort
end

El .lazymétodo devuelve un enumerador perezoso. Llamar .selecto .mapen un enumerador perezoso devuelve otro enumerador perezoso. Solo una vez que llama .uniq, realmente fuerza al enumerador y devuelve una matriz. Entonces, lo que sucede efectivamente es que sus llamadas .selecty .mapse combinan en una: solo se repite una @linesvez para hacer ambas .selecty.map .

Mi instinto es que el reducemétodo de Adam será un poco más rápido, pero creo que es mucho más legible.


La consecuencia principal de esto es que no se crean objetos de matriz intermedios para cada llamada de método subsiguiente. En una @lines.select.mapsituación normal , selectdevuelve una matriz que luego es modificada por map, devolviendo nuevamente una matriz. En comparación, la evaluación diferida solo crea una matriz una vez. Esto es útil cuando su objeto de colección inicial es grande. También le permite trabajar con enumeradores infinitos, por ejemplo random_number_generator.lazy.select(&:odd?).take(10).

Henrebotha
fuente
4
A cada uno lo suyo. Con mi tipo de solución, puedo echar un vistazo a los nombres de los métodos y saber inmediatamente que voy a transformar un subconjunto de los datos de entrada, hacerlos únicos y ordenarlos. reducecomo una transformación de "hacer todo" siempre me parece bastante complicado.
henrebotha
2
@henrebotha: Perdóname si he entendido mal lo que querías decir, pero este es un punto muy importante: no es correcto decir que "solo repites una @linesvez para hacer ambas .selecty .map". El uso .lazyno significa que las operaciones encadenadas en un enumerador perezoso se "colapsen" en una sola iteración. Este es un malentendido común de la evaluación perezosa con operaciones de encadenamiento sobre una colección. (Puede probar esto agregando una putsdeclaración al principio de los bloques selecty mapen el primer ejemplo. Verá que imprimen el mismo número de líneas)
pje
1
@henrebotha: y si lo quitas, .lazyse imprime la misma cantidad de veces. Ese es mi punto: su mapbloqueo y su selectbloqueo se ejecutan el mismo número de veces en las versiones perezosa y ansiosa. La versión perezosa no "combina sus llamadas .selecty .map"
pje
1
@pje: En efecto, los lazy combina porque un elemento que falla la selectcondición no se pasa al map. En otras palabras: anteponer lazyes más o menos equivalente a reemplazar selecty mapcon un bloque único reduce([]), y "inteligentemente" haciendo que selectel bloque sea una condición previa para la inclusión en reduceel resultado de.
henrebotha
1
@henrebotha: Creo que es una analogía engañosa para la evaluación perezosa en general, porque la pereza no cambia la complejidad temporal de este algoritmo. Este es mi punto: en todos los casos, un select-then-map perezoso siempre realizará el mismo número de cálculos que su versión ansiosa. No acelera nada, simplemente cambia el orden de ejecución de cada iteración; la última función de la cadena "extrae" los valores necesarios de las funciones anteriores en orden inverso.
pje
13

Si tiene un selectque puede usar el caseoperador ( ===), grepes una buena alternativa:

p [1,2,'not_a_number',3].grep(Integer){|x| -x } #=> [-1, -2, -3]

p ['1','2','not_a_number','3'].grep(/\D/, &:upcase) #=> ["NOT_A_NUMBER"]

Si necesitamos una lógica más compleja podemos crear lambdas:

my_favourite_numbers = [1,4,6]

is_a_favourite_number = -> x { my_favourite_numbers.include? x }

make_awesome = -> x { "***#{x}***" }

my_data = [1,2,3,4]

p my_data.grep(is_a_favourite_number, &make_awesome) #=> ["***1***", "***4***"]
hirolau
fuente
No es una alternativa, es la única respuesta correcta a la pregunta.
inopinatus
@inopinatus: Ya no . Sin embargo, esta sigue siendo una buena respuesta. No recuerdo haber visto grep con un bloque de otra manera.
Eric Duminil
8

No estoy seguro de que haya uno. El módulo Enumerable , que agrega selectymap , no muestra uno.

Se le pedirá que pase en dos cuadras hasta el select_and_transform método, lo que sería un poco intuitivo en mi humilde opinión.

Obviamente, podría simplemente encadenarlos, lo que es más legible:

transformed_list = lines.select{|line| ...}.map{|line| ... }
Gishu
fuente
3

Respuesta simple:

Si tiene n registros y lo desea selecty en mapfunción de la condición,

records.map { |record| record.attribute if condition }.compact

Aquí, el atributo es lo que desee del registro y la condición que puede marcar.

compacto es eliminar los cero innecesarios que surgieron de esa condición si

Sk. Irfan
fuente
1
Puede usar lo mismo con la condición a menos que también. Como preguntó mi amigo.
Sk. Irfan
2

No, pero puedes hacerlo así:

lines.map { |line| do_some_action if check_some_property  }.reject(&:nil?)

O mejor:

lines.inject([]) { |all, line| all << line if check_some_property; all }
Daniel O'Hara
fuente
14
reject(&:nil?)es básicamente lo mismo que compact.
Jörg W Mittag
Sí, entonces el método de inyección es aún mejor.
Daniel O'Hara
2

Creo que de esta manera es más legible, porque divide las condiciones del filtro y el valor mapeado mientras queda claro que las acciones están conectadas:

results = @lines.select { |line|
  line.should_include?
}.map do |line|
  line.value_to_map
end

Y, en su caso específico, elimine la resultvariable por completo:

def example
  @lines.select { |line|
    line.should_include?
  }.map { |line|
    line.value_to_map
  }.uniq.sort
end
fotanus
fuente
1
def example
  @lines.select {|line| ... }.map {|line| ... }.uniq.sort
end

En Ruby 1.9 y 1.8.7, también puede encadenar y envolver iteradores simplemente sin pasarles un bloque:

enum.select.map {|bla| ... }

Pero no es realmente posible en este caso, ya que los tipos de bloque devuelven valores de selecty mapno coinciden. Tiene más sentido para algo como esto:

enum.inject.with_index {|(acc, el), idx| ... }

AFAICS, lo mejor que puede hacer es el primer ejemplo.

He aquí un pequeño ejemplo:

%w[a b 1 2 c d].map.select {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["a", "b", "c", "d"]

%w[a b 1 2 c d].select.map {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["A", "B", false, false, "C", "D"]

Pero lo que realmente quieres es ["A", "B", "C", "D"].

Jörg W Mittag
fuente
Anoche hice una búsqueda web muy breve de "encadenamiento de métodos en Ruby" y parecía que no estaba bien soportado. Aunque, probablemente debería haberlo intentado ... también, ¿por qué dices que los tipos de argumentos de bloque no coinciden? En mi ejemplo, ambos bloques toman una línea de texto de mi matriz, ¿verdad?
Seth Petry-Johnson
@Seth Petry-Johnson: Sí, lo siento, me refiero a los valores de retorno. selectdevuelve un valor de tipo booleano que decide si conservar el elemento o no, mapdevuelve el valor transformado. El valor transformado en sí probablemente será verdadero, por lo que se seleccionan todos los elementos.
Jörg W Mittag
1

Debería intentar usar mi biblioteca Rearmed Ruby en la que agregué el método Enumerable#select_map. He aquí un ejemplo:

items = [{version: "1.1"}, {version: nil}, {version: false}]

items.select_map{|x| x[:version]} #=> [{version: "1.1"}]
# or without enumerable monkey patch
Rearmed.select_map(items){|x| x[:version]}
Weston Ganger
fuente
select_mapen esta biblioteca simplemente implementa la misma select { |i| ... }.map { |i| ... }estrategia de muchas respuestas anteriores.
Jordan Sitkin
1

Si no desea crear dos matrices diferentes, puede usarlo, compact!pero tenga cuidado.

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}
new_array.compact!

Curiosamente, compact!hace una eliminación in situ de nil. El valor de retorno de compact!es la misma matriz si hubo cambios, pero nulo si no hubo nulos.

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}.tap { |array| array.compact! }

Sería un trazador de líneas.

bibstha
fuente
0

Tu versión:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

Mi version:

def example
  results = {}
  @lines.each{ |line| results[line] = true if ... }
  return results.keys.sort
end

Esto hará 1 iteración (excepto la ordenación) y tiene la ventaja adicional de mantener la singularidad (si no le importa uniq, simplemente haga que los resultados sean una matriz y results.push(line) if ...

Jordan Michael corriendo
fuente
-1

He aquí un ejemplo. No es lo mismo que su problema, pero puede ser lo que desee o puede dar una pista de su solución:

def example
  lines.each do |x|
    new_value = do_transform(x)
    if new_value == some_thing
      return new_value    # here jump out example method directly.
    else
      next                # continue next iterate.
    end
  end
end
zw963
fuente