Карта и Удалить значения nil в Ruby

У меня есть карта, которая либо меняет значение, либо устанавливает его на ноль. Затем я хочу удалить ноль записей из списка. Список не нужно хранить.

Это то, что у меня сейчас есть:

# A simple example function, which returns a value or nil
def transform(n)
  rand > 0.5 ? n * 10 : nil }
end

items.map! { |x| transform(x) } # [1, 2, 3, 4, 5] => [10, nil, 30, 40, nil]
items.reject! { |x| x.nil? } # [10, nil, 30, 40, nil] => [10, 30, 40]

Я знаю, что мог бы просто сделать цикл и условно собрать его в другой массив, например так:

new_items = []
items.each do |x|
    x = transform(x)
    new_items.append(x) unless x.nil?
end
items = new_items

Но это не кажется идиоматическим. Есть ли хороший способ отобразить функцию на список, удаляя/исключая nils по мере продвижения?

Ответ 1

Вы можете использовать compact:

[1, nil, 3, nil, nil].compact
=> [1, 3] 

Я хотел бы напомнить людям, что если вы получаете массив, содержащий nils, в качестве вывода блока map, и этот блок пытается условно вернуть значения, то у вас есть запах кода и вам нужно переосмыслить свою логику.

Например, если вы делаете что-то, что делает это:

[1,2,3].map{ |i|
  if i % 2 == 0
    i
  end
}
# => [nil, 2, nil]

Тогда не надо. Вместо этого до map, reject вещи, которые вы не хотите, или select, которые вы хотите:

[1,2,3].select{ |i| i % 2 == 0 }.map{ |i|
  i
}
# => [2]

Я считаю, что использование compact для очистки беспорядка - это последняя попытка избавиться от вещей, с которыми мы не справились должным образом, обычно потому, что мы не знали, что нас ожидает. Мы всегда должны знать, какие данные используются в нашей программе; Неожиданные/неизвестные данные плохие. Каждый раз, когда я вижу nils в массиве, над которым я работаю, я копаюсь, почему они существуют, и вижу, могу ли я улучшить код, генерирующий массив, вместо того, чтобы позволить Ruby тратить время и память на генерацию nils, а затем просеивать массив, чтобы удалить их позже.

'Just my $%0.2f.' % [2.to_f/100]

Ответ 2

Попробуйте использовать #reduce или #inject!

[1, 2, 3].reduce([]) { |memo, i|
  if i % 2 == 0
    memo << i
  end

  memo
}

Я согласен с принятым ответом, что мы не должны сопоставлять и компилировать, но не по тем же причинам!

Я чувствую себя глубоко внутри того, что map-then-compact эквивалентно select-then-map. Рассмотрим: отображение является взаимно однозначной функцией. Если вы сопоставляете какой-то набор значений и вы набираете карту, то вы хотите, чтобы одно значение в выходном наборе для каждого значения во входном наборе. Если вам нужно выбрать заранее, то вам, вероятно, не нужна карта на множестве. Если вам нужно выбрать потом (или компактно), то, вероятно, вам не нужна карта на множестве. В любом случае вы повторяете два раза по всему набору, когда сокращение нужно только один раз.

Кроме того, на английском языке вы пытаетесь "свести набор целых чисел в набор четных целых чисел".

Ответ 3

В вашем примере:

items.map! { |x| process_x url } # [1, 2, 3, 4, 5] => [1, nil, 3, nil, nil]

не похоже, что значения изменились иначе, чем заменены на nil. Если это так, то:

items.select{|x| process_x url}

будет достаточно.

Ответ 4

Если вам нужен более слабый критерий для отклонения, например, чтобы отклонить пустые строки и нуль, вы можете использовать:

[1, nil, 3, 0, ''].reject(&:blank?)
 => [1, 3, 0] 

Если вы хотите пойти дальше и отклонить нулевые значения (или применить более сложную логику к процессу), вы можете передать блок для отклонения:

[1, nil, 3, 0, ''].reject do |value| value.blank? || value==0 end
 => [1, 3]

[1, nil, 3, 0, '', 1000].reject do |value| value.blank? || value==0 || value>10 end
 => [1, 3]

Ответ 5

@The Tin Man, nice - я не знаю этого метода. Ну, определенно компактный - лучший способ, но все же можно сделать и с простой подстановкой:

[1, nil, 3, nil, nil] - [nil]
 => [1, 3]

Ответ 6

each_with_object, вероятно, самый чистый способ пойти сюда:

new_items = items.each_with_object([]) do |x, memo|
    ret = process_x(x)
    memo << ret unless ret.nil?
end

На мой взгляд, each_with_object лучше, чем inject/reduce в условных случаях, потому что вам не нужно беспокоиться о возвращаемом значении блока.

Ответ 7

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

В вашем случае, поскольку блок оценивается как Falsey, просто:

items.filter_map { |x| process_x url }

Вот хорошее прочтение по теме с некоторыми оценками производительности по сравнению с некоторыми из более ранних подходов к этой проблеме:

N = 1_00_000
enum = 1.upto(1_000)
Benchmark.bmbm do |x|
  x.report("select + map")  { N.times { enum.select { |i| i.even? }.map{|i| i + 1} } }
  x.report("map + compact") { N.times { enum.map { |i| i + 1 if i.even? }.compact } }
  x.report("filter_map")    { N.times { enum.filter_map { |i| i + 1 if i.even? } } }
end

# Rehearsal -------------------------------------------------
# select + map    8.569651   0.051319   8.620970 (  8.632449)
# map + compact   7.392666   0.133964   7.526630 (  7.538013)
# filter_map      6.923772   0.022314   6.946086 (  6.956135)
# --------------------------------------- total: 23.093686sec
# 
#                     user     system      total        real
# select + map    8.550637   0.033190   8.583827 (  8.597627)
# map + compact   7.263667   0.131180   7.394847 (  7.405570)
# filter_map      6.761388   0.018223   6.779611 (  6.790559)

Надеюсь, что это полезно для кого-то!

Ответ 8

Еще один способ выполнить это будет, как показано ниже. Здесь мы используем Enumerable#each_with_object для сбора значений и используем Object#tap, чтобы избавиться от временной переменной, которая в противном случае необходима для nil проверить результат метода process_x.

items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}

Полный пример для иллюстрации:

items = [1,2,3,4,5]
def process x
    rand(10) > 5 ? nil : x
end

items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}

Альтернативный подход:

Изучив метод, который вы вызываете process_x url, неясно, в чем заключается цель ввода x в этом методе. Если я предполагаю, что вы собираетесь обработать значение x, передав ему несколько url и определите, какой из x действительно обрабатывается в действительные результаты, отличные от нуля, тогда может быть Enumerabble.group_by является лучшим вариантом, чем Enumerable#map.

h = items.group_by {|x| (process x).nil? ? "Bad" : "Good"}
#=> {"Bad"=>[1, 2], "Good"=>[3, 4, 5]}

h["Good"]
#=> [3,4,5]