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ą?


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] 

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

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!


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).


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

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

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


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 }

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

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


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

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.


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 ...


-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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.