Есть ли в Ruby метод Array, который сочетает в себе select и map?

96

У меня есть массив 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

Ответы:

115

Я обычно использую 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
Ага, я пытался понять, как игнорировать ноль, возвращаемую моим блоком карты. Спасибо!
Сет Петри-Джонсон,
Нет проблем, я люблю компактные. он ненавязчиво сидит и делает свое дело. Я также предпочитаю этот метод связыванию перечислимых функций для простых критериев выбора, потому что он очень декларативен.
Джед Шнайдер
4
Я не был уверен, что map+ compactдействительно будет работать лучше, injectи опубликовал результаты моих тестов в соответствующем потоке: stackoverflow.com/questions/310426/list-comprehension-in-ruby/…
knuton
3
это приведет к удалению всех нулевых значений, как исходных, так и тех, которые не соответствуют вашим критериям. Так что
будьте осторожны
1
Она не полностью устранить сцепление mapи select, это просто , что compactэто особый случай , rejectкоторый работает на Nils и выполняет несколько лучше из - за того , были реализованы непосредственно в С.
Джо Atzberger
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исхожу из функционального фона, поэтому я нашел первым 😊
Адам Линдберг
35

Рубин 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]

Вот хорошее чтение по этой теме .

Надеюсь, это кому-то пригодится!

SRack
источник
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звонки»
pje
1
@pje: Фактически lazy объединяет их, потому что элемент, который не соответствует selectусловию, не передается в map. Другими словами: добавление в начало lazyпримерно эквивалентно замене selectи mapодним reduce([]), и «разумно» превращение selectблока в предварительное условие для включения в reduceрезультат.
henrebotha
1
@henrebotha: Я думаю, что это вводящая в заблуждение аналогия с ленивым вычислением в целом, потому что лень не меняет временную сложность этого алгоритма. Вот моя точка зрения: в каждом случае ленивое выделение-затем-карта всегда будет выполнять то же количество вычислений, что и его активная версия. Он ничего не ускоряет, он просто меняет порядок выполнения каждой итерации - последняя функция в цепочке при необходимости «вытягивает» значения из предыдущих функций в обратном порядке.
pje
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методу, что было бы немного неинтуитивно, ИМХО.

Очевидно, вы можете просто связать их вместе, что более читабельно:

transformed_list = lines.select{|line| ...}.map{|line| ... }
Гишу
источник
3

Простой ответ:

Если у вас есть n записей, и вы хотите selectи в mapзависимости от условия, тогда

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

Здесь атрибут - это все, что вы хотите от записи, а условие вы можете поставить любой галочкой.

compact - это смыть ненужные нули, которые возникли в результате условия if

Sk. Ирфан
источник
1
Вы можете использовать то же самое с условием if. Как спросил мой друг.
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», и мне показалось, что это плохо поддерживается. Но, наверное, мне следовало попробовать ... также, почему вы говорите, что типы аргументов блока не совпадают? В моем примере оба блока берут строку текста из моего массива, верно?
Сет Петри-Джонсон,
@Seth Petry-Johnson: Да, извините, я имел в виду возвращаемые значения. 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!что вместо удаления nil. Возвращаемое значение compact!- тот же массив, если были изменения, но nil, если не было nils.

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
zw963
источник