In Ruby, esiste un metodo Array che combina "select" e "map"?


96

Ho un array Ruby contenente alcuni valori di stringa. Ho bisogno di:

  1. Trova tutti gli elementi che corrispondono a un predicato
  2. Esegui gli elementi corrispondenti attraverso una trasformazione
  3. Restituisce i risultati come un array

In questo momento la mia soluzione è simile a questa:

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

Esiste un metodo Array o Enumerable che combina selezione e mappatura in un'unica istruzione logica?


5
Non esiste un metodo al momento, ma una proposta per aggiungerne uno a Ruby: bugs.ruby-lang.org/issues/5663
stefankolb

Il Enumerable#grepmetodo fa esattamente ciò che è stato richiesto ed è presente in Ruby da oltre dieci anni. Richiede un argomento predicato e un blocco di trasformazione. @hirolau dà l'unica risposta corretta a questa domanda.
inopinatus

2
Ruby 2.7 sta introducendo filter_mapper questo scopo esatto. Maggiori info qui .
SRack

Risposte:


115

Di solito uso mape compactinsieme ai miei criteri di selezione come suffisso if. compactsi sbarazza dei nils.

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] 

1
Ah-ha, stavo cercando di capire come ignorare i valori zero restituiti dal mio blocco di mappa. Grazie!
Seth Petry-Johnson

Nessun problema, adoro le compatte. si siede discretamente là fuori e fa il suo lavoro. Preferisco anche questo metodo al concatenamento di funzioni enumerabili per semplici criteri di selezione perché è molto dichiarativo.
Jed Schneider

4
Ero incerto se map+ compactsarebbe davvero un rendimento migliore rispetto injecte pubblicato le mie risultati dei benchmark a un filo correlato: stackoverflow.com/questions/310426/list-comprehension-in-ruby/...
knuton

3
questo rimuoverà tutti i nil, sia i nil originali che quelli che non soddisfano i tuoi criteri. Quindi attenzione
utente1143669

1
Esso non del tutto eliminare il concatenamento mape select, è solo che compactè un caso particolare rejectche funziona su Nils ed esegue un po 'meglio a causa di avere stato implementato direttamente in C.
Joe Atzberger

53

Puoi usare reduceper questo, che richiede un solo passaggio:

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

In altre parole, inizializza lo stato in modo che sia quello che desideri (nel nostro caso, un elenco vuoto da riempire :) [], quindi assicurati sempre di restituire questo valore con modifiche per ogni elemento nell'elenco originale (nel nostro caso, l'elemento modificato inserito nell'elenco).

Questo è il più efficiente poiché scorre l'elenco con un solo passaggio ( map+ selecto compactrichiede due passaggi).

Nel tuo caso:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end

20
Non ha each_with_objectun po 'più di senso? Non è necessario restituire l'array alla fine di ogni iterazione del blocco. Puoi semplicemente farlo my_array.each_with_object([]) { |i, a| a << i if i.condition }.
henrebotha

@henrebotha Forse lo fa. Vengo da un background funzionale, ecco perché ho trovato reduceprima 😊
Adam Lindberg

34

Ruby 2.7+

Adesso c'è!

Ruby 2.7 sta introducendo proprio filter_mapper questo scopo. È idiomatico e performante, e mi aspetto che diventi la norma molto presto.

Per esempio:

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

Ecco una buona lettura sull'argomento .

Spero sia utile a qualcuno!


1
Non importa quanto spesso esegua l'upgrade, una funzionalità interessante è sempre nella versione successiva.
mlt

Bello. Un problema potrebbe essere che da quando filter, selecte find_allsono sinonimi, altrettanto mape collectsono, potrebbe essere difficile ricordare il nome di questo metodo. E ' filter_map, select_collect, find_all_mapo filter_collect?
Eric Duminil

19

Un altro modo diverso di affrontare questo problema è usare il nuovo (relativo a questa domanda) Enumerator::Lazy:

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

Il .lazymetodo restituisce un enumeratore pigro. La chiamata .selecto .mapsu un enumeratore pigro restituisce un altro enumeratore pigro. Solo una volta chiamato .uniq, forza effettivamente l'enumeratore e restituisce un array. Quindi ciò che effettivamente accade è che le tue chiamate .selecte .mapsono combinate in una sola: ripeti solo @linesuna volta per fare entrambe le cose .selecte .map.

Il mio istinto è che il reducemetodo di Adam sarà un po 'più veloce, ma penso che questo sia molto più leggibile.


La conseguenza principale di ciò è che non viene creato alcun oggetto array intermedio per ogni successiva chiamata al metodo. In una @lines.select.mapsituazione normale , selectrestituisce un array che viene quindi modificato da map, restituendo nuovamente un array. In confronto, la valutazione pigra crea una matrice solo una volta. Ciò è utile quando il tuo oggetto di raccolta iniziale è grande. Ti consente anche di lavorare con infiniti enumeratori, ad es random_number_generator.lazy.select(&:odd?).take(10).


4
A ognuno il suo. Con il mio tipo di soluzione posso dare un'occhiata ai nomi dei metodi e sapere immediatamente che trasformerò un sottoinsieme dei dati di input, renderlo unico e ordinarlo. reducecome una trasformazione "fai tutto" mi sembra sempre abbastanza complicata.
henrebotha

2
@henrebotha: Perdonami se ho frainteso quello che volevi dire, ma questo è un punto molto importante: non è corretto dire che "ripeti solo @linesuna volta per fare entrambe le cose .selecte .map". L'uso .lazynon significa che le operazioni di operazioni concatenate su un enumeratore pigro verranno "compresse" in una singola iterazione. Si tratta di un malinteso comune della valutazione pigra rispetto alle operazioni di concatenamento su una raccolta. (Puoi verificarlo aggiungendo putsun'istruzione all'inizio dei blocchi selecte mapnel primo esempio. Scoprirai che stampano lo stesso numero di righe)
pje

1
@henrebotha: e se rimuovi il .lazy, stampa lo stesso numero di volte. Questo è il punto: il tuo mapblocco e il tuo selectblocco vengono eseguiti lo stesso numero di volte nelle versioni pigra e desiderosa. La versione pigra non "combina le tue .selecte .mapchiamate"
pje

1
@pje: in effetti lazy li combina perché un elemento che non selectsupera la condizione non viene passato al file map. In altre parole: anteporre lazyè più o meno equivalente a sostituire selecte mapcon un unico blocco di reduce([])"intelligentemente" select, una precondizione per l'inclusione nel reducerisultato di.
henrebotha

1
@henrebotha: Penso che sia un'analogia fuorviante per la valutazione pigra in generale, perché la pigrizia non cambia la complessità temporale di questo algoritmo. Questo è il mio punto: in ogni caso, una lazy select-then-map eseguirà sempre lo stesso numero di calcoli della sua versione desiderosa. Non accelera nulla, cambia semplicemente l'ordine di esecuzione di ogni iterazione: l'ultima funzione della catena "estrae" i valori secondo le necessità dalle funzioni precedenti in ordine inverso.
pje

13

Se hai un selectche può usare l' caseoperatore ( ===), grepè una buona 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"]

Se abbiamo bisogno di una logica più complessa, possiamo creare lambda:

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***"]

Non è un'alternativa: è l'unica risposta corretta alla domanda.
inopinatus

@inopinatus: non più . Questa è comunque una buona risposta. Non ricordo di aver visto grep con un blocco altrimenti.
Eric Duminil

8

Non sono sicuro che ce ne sia uno. Il modulo Enumerable , che aggiunge selecte map, non ne mostra uno.

Ti verrà richiesto di passare in due blocchi al select_and_transformmetodo, il che sarebbe un po 'poco intuitivo IMHO.

Ovviamente, potresti semplicemente concatenarli insieme, il che è più leggibile:

transformed_list = lines.select{|line| ...}.map{|line| ... }

3

Risposta semplice:

Se hai n record e lo desideri selecte in mapbase alla condizione, allora

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

Qui, l'attributo è quello che vuoi dal record e la condizione puoi mettere qualsiasi segno di spunta.

compact è quello di lavare gli inutili nil che sono usciti da quella condizione if


1
Puoi usare lo stesso anche con la condizione a meno. Come ha chiesto il mio amico.
Sk. Irfan

2

No, ma puoi farlo in questo modo:

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

O ancora meglio:

lines.inject([]) { |all, line| all << line if check_some_property; all }

14
reject(&:nil?)è fondamentalmente lo stesso di compact.
Jörg W Mittag

Sì, quindi il metodo di iniezione è ancora migliore.
Daniel O'Hara

2

Penso che in questo modo sia più leggibile, perché divide le condizioni del filtro e il valore mappato pur rimanendo chiaro che le azioni sono collegate:

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

E, nel tuo caso specifico, elimina resulttutte le variabili:

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

1
def example
  @lines.select {|line| ... }.map {|line| ... }.uniq.sort
end

In Ruby 1.9 e 1.8.7, puoi anche concatenare e avvolgere gli iteratori semplicemente non passandogli un blocco:

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

Ma in questo caso non è davvero possibile, poiché i tipi di blocco restituiscono i valori di selecte mapnon corrispondono. Ha più senso per qualcosa del genere:

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

AFAICS, il meglio che puoi fare è il primo esempio.

Ecco un piccolo esempio:

%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"]

Ma quello che vuoi veramente è ["A", "B", "C", "D"].


Ieri sera ho fatto una breve ricerca sul web per "concatenamento di metodi in Ruby" e sembrava che non fosse supportato bene. Tuttavia, probabilmente avrei dovuto provarlo ... inoltre, perché dici che i tipi degli argomenti del blocco non corrispondono? Nel mio esempio entrambi i blocchi stanno prendendo una riga di testo dal mio array, giusto?
Seth Petry-Johnson,

@Seth Petry-Johnson: Sì, mi dispiace, intendevo i valori di ritorno. selectrestituisce un valore booleano che decide se mantenere o meno l'elemento, maprestituisce il valore trasformato. Il valore trasformato stesso probabilmente sarà veritiero, quindi tutti gli elementi vengono selezionati.
Jörg W Mittag

1

Dovresti provare a usare la mia libreria Rearmed Ruby in cui ho aggiunto il metodo Enumerable#select_map. Ecco un esempio:

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

select_mapin questa libreria implementa solo la stessa select { |i| ... }.map { |i| ... }strategia da molte risposte sopra.
Jordan Sitkin

1

Se non vuoi creare due array diversi, puoi usarli compact!ma fai attenzione.

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

È interessante notare che compact!esegue una rimozione sul posto di zero. Il valore restituito di compact!è lo stesso array se ci sono stati cambiamenti ma nil se non ci sono stati nil.

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

Sarebbe un one liner.


0

La tua versione:

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

La mia versione:

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

Questo farà 1 iterazione (eccetto l'ordinamento) e ha il vantaggio aggiuntivo di mantenere l'unicità (se non ti interessa uniq, allora fai semplicemente un array e results.push(line) if ...


-1

Ecco un esempio. Non è lo stesso del tuo problema, ma potrebbe essere quello che vuoi, o può dare un indizio per la tua soluzione:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.