_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 был бы хорошим кандидатом, но мне нужно было изменить ввод, чтобы у меня была бухгалтерская переменная, отслеживающая «перенесено».

Ссылка