Чи існує в Ruby метод Array, який поєднує 'select' та 'map'?


95

У мене є масив Ruby, що містить деякі рядкові значення. Мені потрібно:

  1. Знайдіть усі елементи, які відповідають якомусь присудку
  2. Запустіть відповідні елементи за допомогою перетворення
  3. Повернути результати як масив

Зараз моє рішення виглядає так:

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

Чи існує метод Array або Enumerable, який поєднує в собі вибір та відображення в єдине логічне твердження?


5
На даний момент не існує методу, але пропонується додати його до Ruby: bugs.ruby-lang.org/issues/5663
stefankolb

Enumerable#grepМетод робить саме те , що було запропоновано і вже в Рубіні протягом більше десяти років. Він приймає аргумент предиката та блок перетворення. @hirolau дає єдину правильну відповідь на це питання.
inopinatus

2
Ruby 2.7 представляє саме filter_mapз цією метою. Більше інформації тут .
SRack

Відповіді:


114

Я зазвичай використовую mapі compactразом разом з моїми критеріями відбору в якості постфікса if. compactпозбавляється від нулів.

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
А-ха, я намагався зрозуміти, як ігнорувати нулі, повернені моїм блоком карти. Дякую!
Сет Петрі-Джонсон,

Не біда, я люблю компактний. воно ненав’язливо сидить там і робить свою справу. Я також віддаю перевагу цьому методу, ніж ланцюжку безлічі функцій для простих критеріїв відбору, оскільки він дуже декларативний.
Jed Schneider

4
Я був впевнений , якщо map+ compactбуде дійсно краще , ніж injectта опублікував свої результати тестів в спорідненої теми: stackoverflow.com/questions/310426/list-comprehension-in-ruby / ...
knuton

3
це видалить усі нулі, як початкові, так і ті, які не відповідають вашим критеріям. Тож
стережіться

1
Це не повністю усуває ланцюжки, mapі selectце просто compactособливий випадок, rejectякий працює на nils і працює дещо краще завдяки тому, що був реалізований безпосередньо в C.
Джо Ацбергер,

53

Ви можете використовувати reduceдля цього, що вимагає лише одного проходу:

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

Іншими словами, ініціалізуйте стан таким, яким ви хочете (у нашому випадку порожній список для заповнення:) [], а потім завжди переконайтеся, що повертаєте це значення із змінами для кожного елемента у вихідному списку (у нашому випадку модифікований елемент перенесено до списку).

Це найефективніше, оскільки він перебирає список лише одним проходом ( map+ selectабо compactвимагає двох проходів).

У вашому випадку:

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

20
Не має each_with_objectсенсу трохи більше? Вам не потрібно повертати масив в кінці кожної ітерації блоку. Ви можете просто зробити my_array.each_with_object([]) { |i, a| a << i if i.condition }.
henrebotha

@henrebotha Можливо, так і є. Я reduceпоходжу з функціонального походження, ось чому я знайшов перший 😊
Адам Ліндберг,

34

Рубін 2.7+

Є зараз!

Ruby 2.7 представляє саме filter_mapз цією метою. Це ідіоматично та ефективно, і я сподівався б, що це стане нормою дуже скоро.

Наприклад:

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

Ось гарне читання на цю тему .

Сподіваюся, це комусь корисно!


1
Як би часто я не оновлював, класна функція завжди є в наступній версії.
mlt

Приємно. Одна з проблем , може бути, так як filter, selectі find_allє синонімами, так само , як mapі collectє, це може бути важко запам'ятати ім'я цього методу. Є чи це filter_map, select_collect, find_all_mapабо filter_collect?
Ерік Думініл

19

Інший інший спосіб підходу до цього - використання нового (щодо цього питання) Enumerator::Lazy:

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

.lazyМетод повертає ледачий Нумератор. Виклик .selectабо .mapна лінивому перелічувачі повертає іншого ледачого перелічувача. Лише після того, як ви зателефонуєте .uniq, він фактично змушує перечислювач і повертає масив. Отже, фактично відбувається ваше .selectта .mapдзвінки об’єднуються в одне - ви повторюєте лише @linesодин раз, щоб зробити обидва .selectі .map.

Мій інстинкт полягає в тому, що reduceметод Адама буде трохи швидшим, але я думаю, що це набагато читабельніше.


Основним наслідком цього є те, що для кожного наступного виклику методу не створюються проміжні об'єкти масиву. У звичайній @lines.select.mapситуації selectповертає масив, який потім модифікується map, знову повертаючи масив. Для порівняння, лінива оцінка створює масив лише один раз. Це корисно, коли початковий об'єкт колекції великий. Це також дозволяє вам працювати з нескінченними перечислювачами - наприклад random_number_generator.lazy.select(&:odd?).take(10).


4
Кожному своє. За допомогою свого різновиду рішень я можу поглянути на назви методів і одразу зрозуміти, що збираюся перетворити підмножину вхідних даних, зробити їх унікальними та відсортувати. reduceоскільки трансформація "зроби все" мені завжди здається досить безладною.
henrebotha

2
@henrebotha: Вибачте мене, якщо я неправильно зрозумів, що ви мали на увазі, але це дуже важливий момент: невірно говорити, що "ви повторюєте лише @linesодин раз, щоб зробити і те, .selectі інше .map". Використання .lazyне означає, що операції з ланцюговими операціями над лінивим перечислювачем будуть "згорнуті" в одну ітерацію. Це поширене непорозуміння лінивих оціночних операцій, пов'язаних ланцюгами над колекцією. (Ви можете перевірити це, додавши putsзаяву на початок блоків selectі mapв першому прикладі. Ви побачите, що вони друкують однакову кількість рядків)
пі

1
@henrebotha: і якщо ви видалите .lazyйого, друк буде стільки ж разів. У цьому моя суть - ваш mapблок і ваш selectблок виконуються однаково стільки разів у ледачих та нетерплячих версіях. Ледача версія не "поєднує ваші .selectта .mapдзвінки"
пі

1
@pje: Фактично lazy поєднує їх, оскільки елемент, який не виконує selectумову, не передається в map. Іншими словами: попереднє додавання lazyприблизно еквівалентно заміні selectі mapодним reduce([]), і "розумно" робить selectблок попередньою умовою включення в reduceрезультат.
henrebotha

1
@henrebotha: Я думаю, що це оманлива аналогія для ледачого оцінювання загалом, оскільки лінь не змінює часової складності цього алгоритму. Це моя думка: у кожному випадку лінива карта «виберіть-потім-карта» завжди буде виконувати таку ж кількість обчислень, як і її нетерпляча версія. Це нічого не прискорює, воно просто змінює порядок виконання кожної ітерації - остання функція в ланцюжку "витягує" значення за попередніх функцій у зворотному порядку.
пі

13

Якщо у вас є, selectякий може використовувати caseоператор ( ===), grepце гарна альтернатива:

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

Якщо нам потрібна більш складна логіка, ми можемо створити лямбди:

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

Це не альтернатива - це єдина правильна відповідь на запитання.
inopinatus

@inopinatus: Більше ні . Однак це все ще хороша відповідь. Я не пам’ятаю, щоб інакше бачив grep із блоком.
Ерік Думініл

8

Я не впевнений, що такий є. Модуль Enumerable , який додає selectта mapне показує його.

Вам потрібно буде перейти у два блоки до select_and_transformметоду, який був би трохи неінтуїтивним IMHO.

Очевидно, ви можете просто зв'язати їх ланцюгами, що є більш читабельним:

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

3

Проста відповідь:

Якщо у вас є п записів, і ви хочете , selectі на mapоснові стану , то

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

Тут атрибут - це все, що ви хочете від запису, і умова, яку ви можете поставити будь-яку перевірку.

компактний - це змити непотрібні нулі, які вийшли з того стану if


1
Ви можете використовувати те ж саме з умовою, якщо також не. Як просив мій друг.
Sk. Irfan

2

Ні, але ви можете зробити це так:

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

Або ще краще:

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

14
reject(&:nil?)в основному те саме, що compact.
Jörg W Mittag

Так, так що метод ін'єкції ще кращий.
Даніель О'Хара,

2

Я думаю, що цей спосіб є більш читабельним, оскільки розділяє умови фільтра та відображене значення, залишаючи зрозумілим, що дії пов’язані:

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

І, у вашому конкретному випадку, виключіть resultзмінну разом:

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

У Ruby 1.9 та 1.8.7 ви також можете ланцюжком та обертати ітератори, просто не передаючи їм блок:

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

Але насправді це неможливо в цьому випадку, оскільки типи блоку повертають значення selectі mapне збігаються. Це має більше сенсу для чогось такого:

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

AFAICS, найкраще, що ви можете зробити, це перший приклад.

Ось невеликий приклад:

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

Але те, що ви насправді хочете, - це ["A", "B", "C", "D"].


Вчора ввечері я здійснив дуже короткий веб-пошук щодо "ланцюжка методів у Ruby", і, здавалося, він не підтримувався добре. Тхо, я, мабуть, мав би спробувати ... також, чому ти кажеш, що типи аргументів блоку не збігаються? У моєму прикладі обидва блоки беруть рядок тексту з мого масиву, так?
Сет Петрі-Джонсон,

@ Сет Петрі-Джонсон: Так, вибачте, я мав на увазі значення повернення. selectповертає логічне значення, яке вирішує, зберігати елемент чи ні, mapповертає перетворене значення. Сама трансформована величина, ймовірно, буде правдивою, тому всі елементи вибираються.
Jörg W Mittag

1

Спробуйте використати мою бібліотеку Rearmed Ruby, до якої я додав метод Enumerable#select_map. Ось приклад:

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у цій бібліотеці якраз реалізована та сама select { |i| ... }.map { |i| ... }стратегія з багатьох відповідей вище.
Йордан Сіткін

1

Якщо ви хочете не створювати два різні масиви, ви можете використовувати, compact!але будьте обережні з цим.

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

Цікаво, що compact!робить на місці видалення нуля. Повернене значення - compact!це той самий масив, якщо були зміни, але нуль, якщо не було нулів.

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

Був би одним вкладишем.


0

Ваша версія:

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

Моя версія:

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

Це зробить 1 ітерацію (крім сортування) і додасть додатковий бонус збереження унікальності (якщо ви не дбаєте про uniq, просто зробіть результати масивом і results.push(line) if ...


-1

Ось приклад. Це не те саме, що ваша проблема, але може бути тим, що ви хочете, або може дати підказку для вашого рішення:

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.