Czy w Rubim jest metoda Array, która łączy „select” i „map”?

96

Mam tablicę Ruby zawierającą wartości ciągów. Potrzebuję:

  1. Znajdź wszystkie elementy, które pasują do jakiegoś predykatu
  2. Przeprowadź transformację dopasowanych elementów
  3. Zwróć wyniki jako tablicę

W tej chwili moje rozwiązanie wygląda następująco:

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

Czy istnieje metoda Array lub Enumerable, która łączy wybieranie i mapowanie w jedną instrukcję logiczną?

Seth Petry-Johnson
źródło
5
W tej chwili nie ma metody, ale propozycja dodania jej do Rubiego: bugs.ruby-lang.org/issues/5663
stefankolb
Enumerable#grepMetoda robi dokładnie to, co został poproszony i została w Ruby od ponad dziesięciu lat. Pobiera argument predykatu i blok transformacji. @hirolau daje jedyną poprawną odpowiedź na to pytanie.
inopinatus
2
W filter_maptym właśnie celu wprowadza się Ruby 2.7 . Więcej informacji tutaj .
SRack

Odpowiedzi:

115

Zwykle używam mapi compactrazem z moimi kryteriami wyboru jako postfiksem if. compactpozbywa się zer.

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
źródło
1
Ah-ha, próbowałem wymyślić, jak zignorować wartości zerowe zwrócone przez mój blok mapy. Dzięki!
Seth Petry-Johnson
Nie ma problemu, uwielbiam kompaktowe. dyskretnie siedzi tam i wykonuje swoją pracę. Wolę również tę metodę niż łańcuchowanie wyliczalnych funkcji dla prostych kryteriów wyboru, ponieważ jest ona bardzo deklaratywna.
Jed Schneider
4
Nie byłem pewien, czy map+ compactnaprawdę działałby lepiej niż injecti opublikowałem wyniki testów porównawczych w powiązanym wątku: stackoverflow.com/questions/310426/list-comprehension-in-ruby/…
knuton
3
spowoduje to usunięcie wszystkich wartości zerowych, zarówno oryginalnych wartości zerowych, jak i tych, które nie spełniają kryteriów. Więc uważaj
user1143669
1
Nie eliminuje całkowicie łańcuchów mapi selectjest to po prostu compactspecjalny przypadek, rejectktóry działa na zerach i działa nieco lepiej, ponieważ został zaimplementowany bezpośrednio w C.
Joe Atzberger
53

Możesz użyć reducedo tego, co wymaga tylko jednego przejścia:

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

Innymi słowy, zainicjuj stan tak, jak chcesz (w naszym przypadku pusta lista do wypełnienia:) [], a następnie zawsze upewnij się, że zwracasz tę wartość z modyfikacjami dla każdego elementu z oryginalnej listy (w naszym przypadku zmodyfikowany element przeniesiony na listę).

Jest to najbardziej wydajne, ponieważ powoduje pętle po liście tylko jednym przebiegiem ( map+ selectlub compactwymaga dwóch przebiegów).

W Twoim przypadku:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end
Adam Lindberg
źródło
20
To nie each_with_objectma większego sensu? Nie musisz zwracać tablicy na końcu każdej iteracji bloku. Możesz po prostu to zrobić my_array.each_with_object([]) { |i, a| a << i if i.condition }.
henrebotha
@henrebotha Być może tak. reducePochodzę z funkcjonalnego zaplecza, dlatego trafiłem jako pierwszy 😊
Adam Lindberg
35

Ruby 2.7+

Jest teraz!

W filter_maptym właśnie celu wprowadza się Ruby 2.7 . Jest idiomatyczny i performatywny i spodziewałbym się, że wkrótce stanie się normą.

Na przykład:

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

Oto dobra lektura na ten temat .

Mam nadzieję, że to komuś się przyda!

SRack
źródło
1
Bez względu na to, jak często aktualizuję, fajna funkcja jest zawsze w następnej wersji.
mlt
Miły. Jednym problemem może być to, że skoro filter, selectifind_all są synonimami, tak jak mapi collectsą, może być trudno zapamiętać nazwę tej metody. Jest to filter_map, select_collect, find_all_maplub filter_collect?
Eric Duminil
19

Innym innym sposobem podejścia jest użycie nowego (w stosunku do tego pytania) Enumerator::Lazy:

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

.lazyMetoda zwraca leniwe wyliczający. Wywołanie .selectlub .mapna leniwym module wyliczającym zwraca inny leniwy moduł wyliczający. Dopiero po wywołaniu .uniqfaktycznie wymusza to moduł wyliczający i zwraca tablicę. Skutecznie się dzieje, .selecta .mappołączenia są łączone w jedno - powtarzasz tylko @linesraz, aby wykonać oba .selecti .map.

Wydaje mi się, że reducemetoda Adama będzie trochę szybsza, ale myślę, że jest to znacznie bardziej czytelne.


Główną konsekwencją tego jest to, że dla każdego kolejnego wywołania metody nie są tworzone żadne pośrednie obiekty tablicy. W normalnej @lines.select.mapsytuacji selectzwraca tablicę, która jest następnie modyfikowana przez map, ponownie zwracając tablicę. Dla porównania, leniwa ocena tworzy tablicę tylko raz. Jest to przydatne, gdy początkowy obiekt kolekcji jest duży. Umożliwia także pracę z nieskończonymi modułami wyliczającymi - np random_number_generator.lazy.select(&:odd?).take(10).

henrebotha
źródło
4
Do każdego własnego. Dzięki takiemu rozwiązaniu mogę rzucić okiem na nazwy metod i od razu wiedzieć, że zamierzam przekształcić podzbiór danych wejściowych, uczynić go unikatowym i posortować. reduceponieważ transformacja typu „robienie wszystkiego” zawsze wydaje mi się dość niechlujna.
henrebotha
2
@henrebotha: Wybacz mi, jeśli źle zrozumiałem, co masz na myśli, ale to jest bardzo ważna kwestia: nie jest poprawne stwierdzenie, że „powtarzasz tylko @linesraz, aby zrobić jedno .selecti drugie .map”. Użycie .lazynie oznacza, że ​​operacje łańcuchowe na leniwym module wyliczającym zostaną „zwinięte” w jedną iterację. Jest to powszechne nieporozumienie dotyczące leniwej oceny w ramach operacji łączenia w łańcuch na kolekcji. (Możesz to sprawdzić, dodając putsinstrukcję na początku bloków selecti mapw pierwszym przykładzie.
Przekonasz się
1
@henrebotha: i jeśli usuniesz ten plik .lazy, drukuje tę samą liczbę razy. O to mi chodzi - twój mapblok i twój selectblok są wykonywane tyle samo razy w wersji leniwej i chętnej. Wersja leniwa nie „łączy Twojego .selecti .mapdzwoni”
pje,
1
@pje: W efekcie lazy łączy je, ponieważ element, który nie spełnia selectwarunku, nie jest przekazywany do map. Innymi słowy: poprzedzanie lazyjest z grubsza równoważne zastępowaniu selecti mappojedynczym reduce([]), a „inteligentnie” uczynienie selectbloku warunkiem wstępnym włączenia do reducewyniku.
henrebotha
1
@henrebotha: Myślę, że to myląca analogia do ogólnie leniwej oceny, ponieważ lenistwo nie zmienia złożoności czasowej tego algorytmu. To jest mój punkt widzenia: w każdym przypadku leniwa mapa typu wybierz, a następnie zawsze wykona taką samą liczbę obliczeń, jak jej chętna wersja. Nie przyspiesza niczego, po prostu zmienia kolejność wykonywania każdej iteracji - ostatnia funkcja w łańcuchu „pobiera” wartości z poprzednich funkcji w odwrotnej kolejności.
pje
13

Jeśli masz, selectże możesz użyć caseoperatora ( ===), grepjest to dobra alternatywa:

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

Jeśli potrzebujemy bardziej złożonej logiki, możemy stworzyć lambdy:

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
źródło
To nie jest alternatywa - to jedyna poprawna odpowiedź na to pytanie.
inopinatus
@inopinatus: Już nie . Jest to jednak nadal dobra odpowiedź. Inaczej nie pamiętam, żebym widział grepa z blokiem.
Eric Duminil
8

Nie jestem pewien, czy istnieje. Moduł Enumerable , która dodaje selecti mapnie wykazuje jeden.

Będziesz musiał przekazać select_and_transformmetodę w dwóch blokach , co byłoby nieco nieintuicyjne w IMHO.

Oczywiście możesz po prostu połączyć je ze sobą, co jest bardziej czytelne:

transformed_list = lines.select{|line| ...}.map{|line| ... }
Gishu
źródło
3

Prosta odpowiedź:

Jeśli masz n rekordów i chcesz, selecti mapna podstawie warunku, to

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

Tutaj atrybut jest tym, co chcesz z rekordu i warunkiem, który możesz sprawdzić.

compact to spłukanie niepotrzebnych zera, które wyszły z tego warunku if

Sk. Irfan
źródło
1
Możesz użyć tego samego z warunkiem, chyba że też. Jak zapytał mój przyjaciel.
Sk. Irfan
2

Nie, ale możesz to zrobić w ten sposób:

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

Albo jeszcze lepiej:

lines.inject([]) { |all, line| all << line if check_some_property; all }
Daniel O'Hara
źródło
14
reject(&:nil?)jest w zasadzie taki sam jak compact.
Jörg W Mittag
Tak, więc metoda wstrzykiwania jest jeszcze lepsza.
Daniel O'Hara
2

Myślę, że ten sposób jest bardziej czytelny, ponieważ dzieli warunki filtru i zmapowaną wartość, pozostając jednocześnie jasnym, że akcje są połączone:

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

W swoim konkretnym przypadku usuń resultzmienną wszystkie razem:

def example
  @lines.select { |line|
    line.should_include?
  }.map { |line|
    line.value_to_map
  }.uniq.sort
end
fotanus
źródło
1
def example
  @lines.select {|line| ... }.map {|line| ... }.uniq.sort
end

W Rubim 1.9 i 1.8.7 możesz także łączyć i zawijać iteratory, po prostu nie przekazując im bloku:

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

Ale w tym przypadku nie jest to naprawdę możliwe, ponieważ typy bloków zwracają wartości selecti mapnie pasują do siebie. Ma to większy sens w przypadku czegoś takiego:

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

AFAICS, najlepsze, co możesz zrobić, to pierwszy przykład.

Oto mały przykład:

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

Ale to, czego naprawdę chcesz, to ["A", "B", "C", "D"].

Jörg W Mittag
źródło
Zeszłej nocy przeprowadziłem bardzo krótkie wyszukiwanie w sieci pod kątem „łączenia metod w Rubim” i wydawało się, że nie jest to dobrze obsługiwane. Chociaż, prawdopodobnie powinienem był tego spróbować ... także, dlaczego mówisz, że typy argumentów blokowych nie pasują do siebie? W moim przykładzie oba bloki pobierają wiersz tekstu z mojej tablicy, prawda?
Seth Petry-Johnson
@Seth Petry-Johnson: Tak, przepraszam, miałem na myśli wartości zwracane. selectzwraca wartość typu Boolean, która decyduje, czy zachować element, czy nie, mapzwraca przekształconą wartość. Przekształcona wartość prawdopodobnie będzie prawdziwa, więc wszystkie elementy zostaną wybrane.
Jörg W Mittag
1

Powinieneś spróbować użyć mojej biblioteki Rearmed Ruby, w której dodałem metodę Enumerable#select_map. Oto przykład:

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
źródło
select_map w tej bibliotece po prostu implementuje to samo select { |i| ... }.map { |i| ... } strategię z wielu odpowiedzi powyżej.
Jordan Sitkin
1

Jeśli nie chcesz tworzyć dwóch różnych tablic, możesz ich użyć, compact!ale zachowaj ostrożność.

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

Co ciekawe, compact!usuwa zero na miejscu. Wartość zwracana compact!jest tą samą tablicą, jeśli wystąpiły zmiany, ale nil, jeśli nie było żadnych wartości zerowych.

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

Byłby to jeden liniowiec.

bibstha
źródło
0

Twoja wersja:

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

Moja wersja:

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

Spowoduje to wykonanie 1 iteracji (z wyjątkiem sortowania) i ma dodatkową zaletę w postaci zachowania unikalności (jeśli nie obchodzi cię unikalność, po prostu utwórz z wyników tablicę i results.push(line) if ...

Jordan Michael Rushing
źródło
-1

Oto przykład. To nie to samo, co twój problem, ale może być tym, czego chcesz lub może dać wskazówkę do rozwiązania:

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
źródło