_1 _ (_ 2_) - один из самых мощных методов, существующих в модуле Enumerable, что означает, что методы доступны в любых экземплярах любого класса, который включает этот модуль, включая Array, Hash, Set. »И Диапазон .
reduce
можно использовать в процессе MapReduce, часто это основа для понимания и отличный способ сгруппировать значения или вычислить одно значение (сокращение набора значений до одного значения) с учетом набор значений.
В этой статье быстро показано, как пропустить значения / условно возвращаемые значения во время reduce
итерации и как преждевременно прервать / вернуть другое значение и остановить итерацию.
Резюме 💬
Из документации для экземпляра enum
(Enumerable), вызывающего enum.reduce
:
# Combines all elements of <i>enum</i> by applying a binary # operation, specified by a block or a symbol that names a # method or operator.
Примером использования сокращения может быть написание функции, которая суммирует все элементы в коллекции:
## # Sums each item in the enumerable (naive) # # @param [Enumerable] enum the enumeration of items to sum # @return [Numeric] the sum # def summation(enum) sum = 0 enum.each do |item| sum += item end sum end ## # Sums each item in the enumerable (reduce block) # # Each iteration the result of the block is the passed in previous_result. # # @param [Enumerable] enum the enumeration of items to sum # @return [Numeric] the sum # def summation(enum) enum.reduce do |previous_result, item| previous_result + item end end ## # Sums each item in the enumerable (reduce method) # # Each iteration the :+ symbol is sent as a message to the current result with # the next value as argument. The result is the new current result. # # @param [Enumerable] enum the enumeration of items to sum # @return [Numeric] the sum # def summation(enum) enum.reduce(:+) end ## # Alias for enum.sum # def summation(enum) enum.sum end
reduce
принимает необязательное начальное значение, которое используется вместо первого элемента коллекции, если оно задано.
Как контролировать поток?
При работе с reduce
вы можете оказаться в одной из двух ситуаций:
- вы хотите условно вернуть другое значение для итерации (которое используется в качестве базового значения для следующей итерации)
- вы хотите вырваться пораньше (полностью прекратить итерацию)
следующий ⏭
Ключевое слово next
позволяет вернуться из блока yield
, что справедливо для любого перечисления.
Допустим, вы суммируете набор чисел, но хотите половину любого четного числа и удвоение любого нечетного числа:
def halfly_even_doubly_odd(enum) enum.reduce(0) do |result, i| result + i * (i.even? ? 0.5 : 2) end end
Не так уж плохо. Но теперь возникает еще одно бизнес-требование - пропускать любое число меньше 5:
def halfly_even_doubly_odd(enum) enum.reduce(0) do |result, i| if i < 5 result else result + i * (i.even? ? 0.5 : 2) end end end
Фу. Это не очень хороший рубиновый код. При использовании next
это могло бы выглядеть так:
def halfly_even_doubly_odd(enum) enum.reduce(0) do |result, i| next result if i < 5 next result + i * 0.5 if i.even? result + i * 2 end end
next
работает в любом перечислении, поэтому, если вы просто обрабатываете элементы с помощью .each
, вы тоже можете его использовать:
(1..10).each do |num| next if num.odd? puts num end # 2 # 4 # 6 # 8 # 10 # => 1..10
перерыв 🛑
Вместо перехода к следующему элементу вы можете полностью остановить итерацию перечислителя, используя break
.
Если у нас те же бизнес-требования, что и раньше, но мы должны вернуть 42, если входящий элемент равен 7, это будет выглядеть так:
def halfly_even_doubly_odd(enum) enum.reduce(0) do |result, i| break 42 if i == 7 next result if i < 5 next result + i * 0.5 if i.even? result + i * 2 end end
Опять же, это работает в любом цикле. Поэтому, если вы используете find
, чтобы попытаться найти элемент в своем перечислении, и хотите изменить возвращаемое значение этой находки, вы можете сделать это с помощью break
:
def find_my_red_item(enum) enum.find do |item| break item.name if item.color == 'red' end end find_my_red_item([ { name: "umbrella", color: "black" }, { name: "shoe", color: "red" }, { name: "pen", color: "blue" } ]) # => 'shoe'
StopIteration
Возможно, вы слышали или видели raise StopIteration
. Это специальное исключение, которое вы можете использовать для остановки итерации блока / перечисления, но его варианты использования ограничены, так как вам не следует пытаться управлять потоком с помощью raise
или fail
. В блоге airbrake есть хорошая статья об этом варианте использования.
Когда использовать сокращение
Если вам нужен совет, когда использовать reduce
, не смотрите дальше. Я использую четыре правила, чтобы определить, нужно ли мне использовать reduce
, each_with_object
или что-то еще.
Я использую reduce
, когда:
- уменьшение набора значений до меньшего результата (например, 1 значение)
- группировка набора значений (по возможности используйте
group_by
) - изменение неизменяемых примитивов / объектов значений (возвращение нового значения)
- вам нужно новое значение (например, новый массив или хэш)
Альтернативы 🔀
Когда вариант использования не соответствует приведенным выше рекомендациям, в большинстве случаев мне действительно нужен each_with_object
, который имеет аналогичную подпись, но не создает новое значение на основе возвращаемого значения блока, а вместо этого выполняет итерацию коллекции с предопределенным « объект », что значительно упрощает использование логики внутри блока:
doubles = (1..10).each_with_object([]) do |num, result| result << num* 2 # same as result.push(num * 2) end # => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] doubles_over_ten = (1..10).each_with_object([]) do |num, result| result << num * 2 if num > 5 end # => [12, 14, 16, 18, 20]
Используйте each_with_object
, когда:
- создание нового контейнера (например, массива или хеша). Обратите внимание, что вы на самом деле не уменьшаете текущую коллекцию до меньшего результата, а вместо этого условно или безоговорочно сопоставляете.
- вам нужна логика в своем блоке без повторения значения
result
(потому что вы должны предоставить возвращаемое значение)
Мой вариант использования
Причина, по которой я изучал поток управления с помощью reduce
, заключается в том, что я просматривал список объектов значений, представляющих миграцию. Без использования lazy
я хотел элегантный способ представления, когда должны выполняться эти миграции, поэтому использовал семантическую версию. Перечисляемый migrations
- это отсортированный список миграций с прикрепленной семантической версией.
migrations.reduce(input) do |migrated, (version, migration)| migrated = migration.call(migrated) next migrated unless current_version.in_range?(version) break migrated end
Функция in_range?
определяет, выполняется ли миграция, на основе текущей «входной» версии и семантической версии миграции. Как только «текущая» версия окажется в пределах допустимого диапазона, она должна выполнить миграцию и остановиться.
Альтернативы были менее благоприятными:
take_while
,select
и друзья могут фильтровать список, но для этого требуется несколько итераций коллекцииmigrations
find
был бы хорошим кандидатом, но мне нужно было изменить ввод, чтобы у меня была бухгалтерская переменная, отслеживающая «перенесено».