Gibt es in Ruby eine Array-Methode, die 'select' und 'map' kombiniert?


95

Ich habe ein Ruby-Array mit einigen Zeichenfolgenwerten. Ich muss einfach:

  1. Finden Sie alle Elemente, die einem Prädikat entsprechen
  2. Führen Sie die übereinstimmenden Elemente durch eine Transformation
  3. Geben Sie die Ergebnisse als Array zurück

Im Moment sieht meine Lösung so aus:

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

Gibt es eine Array- oder Enumerable-Methode, die select und map zu einer einzigen logischen Anweisung kombiniert?


5
Im Moment gibt es keine Methode, aber einen Vorschlag, eine zu Ruby hinzuzufügen: bugs.ruby-lang.org/issues/5663
stefankolb

Die Enumerable#grepMethode macht genau das, was gefragt wurde und ist seit über zehn Jahren in Ruby. Es braucht ein Prädikatargument und einen Transformationsblock. @hirolau gibt die einzig richtige Antwort auf diese Frage.
Inopinatus

2
Ruby 2.7 wird filter_mapgenau für diesen Zweck eingeführt. Mehr Infos hier .
SRack

Antworten:


114

Normalerweise verwende ich mapund compactzusammen mit meinen Auswahlkriterien als Postfix if. compactwird die Nullen los.

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, ich habe versucht herauszufinden, wie ich die von meinem Kartenblock zurückgegebenen Nullen ignorieren kann. Vielen Dank!
Seth Petry-Johnson

Kein Problem, ich liebe kompakt. es sitzt unauffällig da draußen und macht seinen Job. Ich ziehe diese Methode auch der Verkettung von Aufzählungsfunktionen für einfache Auswahlkriterien vor, da sie sehr deklarativ ist.
Jed Schneider

4
Ich war mir nicht sicher, ob map+ compactwirklich besser abschneiden würde als injectund veröffentlichte meine Benchmark-Ergebnisse in einem verwandten Thread: stackoverflow.com/questions/310426/list-comprehension-in-ruby/…
knuton

3
Dadurch werden alle Nullen entfernt, sowohl die ursprünglichen Nullen als auch diejenigen, die Ihre Kriterien nicht erfüllen. Also
aufgepasst

1
Es beseitigt die Verkettung nicht vollständigmap und selectes ist nur compactein Sonderfall reject, der mit Nullen funktioniert und etwas besser abschneidet, da er direkt in C implementiert wurde.
Joe Atzberger

53

Sie können reducedies verwenden, was nur einen Durchgang erfordert:

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

Mit anderen Worten, initialisieren Sie den Status so, wie Sie es möchten (in unserem Fall eine leere Liste, die gefüllt werden muss :) [], und stellen Sie dann immer sicher, dass Sie diesen Wert mit Änderungen für jedes Element in der ursprünglichen Liste (in unserem Fall das geänderte Element) zurückgeben auf die Liste geschoben).

Dies ist am effizientesten, da die Liste nur mit einem Durchgang durchlaufen wird ( map+ selectoder compactzwei Durchgänge erforderlich sind).

In deinem Fall:

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

20
Macht das nicht each_with_objectein bisschen mehr Sinn? Sie müssen das Array nicht am Ende jeder Iteration des Blocks zurückgeben. Sie können einfach tun my_array.each_with_object([]) { |i, a| a << i if i.condition }.
Henrebotha

@henrebotha Vielleicht tut es. Ich komme aus einem funktionalen Hintergrund, deshalb habe ich reducezuerst gefunden 😊
Adam Lindberg

33

Ruby 2.7+

Da ist jetzt!

Ruby 2.7 wird filter_mapgenau für diesen Zweck eingeführt. Es ist idiomatisch und performant, und ich würde erwarten, dass es sehr bald zur Norm wird.

Beispielsweise:

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

Hier ist eine gute Lektüre zu diesem Thema .

Hoffe das ist nützlich für jemanden!


1
Egal wie oft ich aktualisiere, eine coole Funktion ist immer in der nächsten Version enthalten.
Mlt

Nett. Ein Problem könnte sein , dass da filter, selectund find_allsind auch, wie mapund collectsind, könnte es schwierig sein , den Namen dieser Methode zu erinnern. Ist es filter_map, select_collect, find_all_mapoder filter_collect?
Eric Duminil

19

Eine andere Art, dies zu erreichen, ist die Verwendung des Neuen (relativ zu dieser Frage) Enumerator::Lazy:

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

Die .lazyMethode gibt einen Lazy Enumerator zurück. Wenn Sie einen Lazy Enumerator aufrufen .selectoder .mapaktivieren, wird ein anderer Lazy Enumerator zurückgegeben. Erst wenn Sie aufrufen, .uniqwird der Enumerator erzwungen und ein Array zurückgegeben. Was also effektiv passiert, ist, dass Ihre .selectund .mapAnrufe zu einem zusammengefasst werden - Sie iterieren nur @lineseinmal, um beides .selectund zu tun .map.

Mein Instinkt ist, dass Adams reduceMethode etwas schneller sein wird, aber ich denke, dass dies weitaus besser lesbar ist.


Die Hauptfolge davon ist, dass für jeden nachfolgenden Methodenaufruf keine Zwischenarrayobjekte erstellt werden. Gibt in einer normalen @lines.select.mapSituation selectein Array zurück, das dann von geändert wird map, und gibt erneut ein Array zurück. Im Vergleich dazu erstellt die verzögerte Auswertung ein Array nur einmal. Dies ist nützlich, wenn Ihr anfängliches Sammlungsobjekt groß ist. Es ermöglicht Ihnen auch, mit unendlichen Enumeratoren zu arbeiten - z random_number_generator.lazy.select(&:odd?).take(10).


4
Jedem das Seine. Mit meiner Art von Lösung kann ich einen Blick auf die Methodennamen werfen und sofort wissen, dass ich eine Teilmenge der Eingabedaten transformieren, eindeutig machen und sortieren werde. reduceAls "alles tun" -Transformation fühlt sich für mich immer ziemlich chaotisch an.
Henrebotha

2
@henrebotha: Verzeihen Sie mir, wenn ich falsch verstanden habe, was Sie gemeint haben, aber dies ist ein sehr wichtiger Punkt: Es ist nicht richtig zu sagen, dass "Sie nur @lineseinmal iterieren , um beides zu tun .selectund .map". Die Verwendung .lazybedeutet nicht, dass verkettete Operationen Operationen auf einem Lazy Enumerator in einer einzigen Iteration "reduziert" werden. Dies ist ein häufiges Missverständnis der verzögerten Bewertung von Verkettungsvorgängen über eine Sammlung. (Sie können dies testen, indem Sie im ersten Beispiel eine putsAnweisung am Anfang der Blöcke selectund hinzufügen map. Sie werden feststellen, dass sie die gleiche Anzahl von Zeilen drucken)
pje

1
@henrebotha: und wenn Sie das entfernen, wird .lazyes genauso oft gedruckt. Das ist mein Punkt - Ihr mapBlock und Ihr selectBlock werden in den faulen und eifrigen Versionen gleich oft ausgeführt. Die faule Version "kombiniert nicht Ihre .selectund .mapAnrufe"
pje

1
@pje: In der Tat lazy verbindet sie , weil ein Element , das die nicht selecterhalten Bedingung nicht auf den übergebenen map. Mit anderen Worten: Das Voranstellen lazyist ungefähr gleichbedeutend mit dem Ersetzen selectund mapdurch ein einzelnes reduce([])und "intelligent", was selectden Block zu einer Voraussetzung für die Aufnahme in reducedas Ergebnis macht.
Henrebotha

1
@henrebotha: Ich denke, das ist eine irreführende Analogie für die verzögerte Bewertung im Allgemeinen, da Faulheit die zeitliche Komplexität dieses Algorithmus nicht verändert. Dies ist mein Punkt: In jedem Fall führt eine faule Auswahl-dann-Karte immer die gleiche Anzahl von Berechnungen durch wie ihre eifrige Version. Es beschleunigt nichts, es ändert nur die Ausführungsreihenfolge jeder Iteration - die letzte Funktion in der Kette "zieht" Werte nach Bedarf aus den vorherigen Funktionen in umgekehrter Reihenfolge.
pje

13

Wenn Sie eine haben select, die den caseOperator ( ===) verwenden kann, grepist dies eine gute Alternative:

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

Wenn wir eine komplexere Logik benötigen, können wir Lambdas erstellen:

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

Es ist keine Alternative - es ist die einzig richtige Antwort auf die Frage.
Inopinatus

@inopinatus: Nicht mehr . Dies ist jedoch immer noch eine gute Antwort. Ich erinnere mich nicht, dass ich sonst grep mit einem Block gesehen habe.
Eric Duminil

8

Ich bin mir nicht sicher, ob es einen gibt. Das Enumerable-Modul , das selectund hinzufügt map, zeigt keines an.

Sie müssten in zwei Blöcken an die select_and_transformMethode übergeben, was meiner Meinung nach etwas unintuitiv wäre.

Natürlich können Sie sie einfach miteinander verketten, was besser lesbar ist:

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

3

Einfache Antwort:

Wenn Sie n Datensätze haben und möchten selectund mapbasierend auf der Bedingung dann

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

Hier ist das Attribut das, was Sie von dem Datensatz und der Bedingung erwarten, die Sie überprüfen können.

Kompakt ist es, die unnötigen Nullen zu spülen, die aus diesem Zustand hervorgegangen sind


1
Sie können dasselbe auch mit einer anderen Bedingung verwenden. Wie mein Freund fragte.
Sk. Irfan

2

Nein, aber Sie können es so machen:

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

Oder noch besser:

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

14
reject(&:nil?)ist im Grunde das gleiche wie compact.
Jörg W Mittag

Ja, die Injektionsmethode ist also noch besser.
Daniel O'Hara

2

Ich denke, dass dieser Weg besser lesbar ist, da die Filterbedingungen und der zugeordnete Wert aufgeteilt werden, während klar bleibt, dass die Aktionen miteinander verbunden sind:

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

Und eliminieren Sie in Ihrem speziellen Fall die resultVariable insgesamt:

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 und 1.8.7 können Sie Iteratoren auch verketten und umschließen, indem Sie ihnen einfach keinen Block übergeben:

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

In diesem Fall ist dies jedoch nicht wirklich möglich, da die Typen der Blockrückgabewerte von selectund mapnicht übereinstimmen. Für so etwas macht es mehr Sinn:

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

AFAICS, das Beste, was Sie tun können, ist das erste Beispiel.

Hier ist ein kleines Beispiel:

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

Aber was Sie wirklich wollen, ist ["A", "B", "C", "D"].


Ich habe letzte Nacht eine sehr kurze Websuche nach "Methodenverkettung in Ruby" durchgeführt und es schien, als würde es nicht gut unterstützt. Tho, ich hätte es wahrscheinlich versuchen sollen ... auch, warum sagen Sie, dass die Typen der Blockargumente nicht übereinstimmen? In meinem Beispiel nehmen beide Blöcke eine Textzeile aus meinem Array, oder?
Seth Petry-Johnson

@ Seth Petry-Johnson: Ja, sorry, ich meinte die Rückgabewerte. selectGibt einen booleschen Wert zurück, der entscheidet, ob das Element beibehalten werden soll oder nicht. mapGibt den transformierten Wert zurück. Der transformierte Wert selbst wird wahrscheinlich wahr sein, sodass alle Elemente ausgewählt werden.
Jörg W Mittag

1

Sie sollten versuchen, meine Bibliothek Rearmed Ruby zu verwenden, in der ich die Methode hinzugefügt habe Enumerable#select_map. Hier ein Beispiel:

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 dieser Bibliothek implementiert nur die gleiche select { |i| ... }.map { |i| ... }Strategie aus vielen Antworten oben.
Jordan Sitkin

1

Wenn Sie nicht zwei verschiedene Arrays erstellen möchten, können Sie diese verwenden, compact!aber seien Sie vorsichtig.

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

Interessanterweise compact!wird an Ort und Stelle Null entfernt. Der Rückgabewert von compact!ist das gleiche Array, wenn Änderungen vorgenommen wurden, aber Null, wenn keine Nullen vorhanden waren.

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

Wäre ein Einzeiler.


0

Deine Version:

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

Meine Version:

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

Dies führt 1 Iteration durch (mit Ausnahme der Sortierung) und hat den zusätzlichen Vorteil, dass die Eindeutigkeit erhalten bleibt (wenn Sie sich nicht für Uniq interessieren, machen Sie die Ergebnisse einfach zu einem Array und results.push(line) if ...


-1

Hier ist ein Beispiel. Es ist nicht dasselbe wie Ihr Problem, kann aber das sein, was Sie wollen, oder kann einen Hinweis auf Ihre Lösung geben:

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.