Enumerable: map, select, reduce и друзья

Здесь Ruby раскрывается во всей красе. Модуль Enumerable даёт коллекциям десятки методов-преобразований, которые заменяют ручные циклы короткими выразительными цепочками.
Суть: Enumerable — это модуль с методами вроде map (преобразовать каждый), select (отфильтровать), reduce (свернуть в одно значение); все они построены на each и принимают блок.

Это самая важная тема для повседневной работы. Освоив три-четыре метода Enumerable, вы перестаёте писать циклы вообще: вместо «создай массив, пройди, добавь» вы декларативно говорите «преобразуй каждый» или «оставь подходящие».

nums = [1, 2, 3, 4, 5, 6]

# map — преобразовать каждый элемент
puts nums.map { |n| n * n }.inspect       # => [1,4,9,16,25,36]
# select — оставить подходящие
puts nums.select { |n| n.even? }.inspect   # => [2,4,6]
# reject — выбросить подходящие
puts nums.reject(&:even?).inspect          # => [1,3,5]
# reduce — свернуть в одно значение
puts nums.reduce(0) { |sum, n| sum + n }   # => 21
puts nums.sum                              # => 21 (короче)

Разбор: цепочки и группировка

Главная сила в том, что эти методы возвращают новые коллекции и потому соединяются в цепочки. А group_by и each_with_object решают более сложные задачи — группировку и накопление в произвольную структуру.

words = %w[ruby go rust python java]

# цепочка: длинные слова --> в верхний регистр --> отсортировать
result = words.select { |w| w.length > 3 }
              .map(&:upcase)
              .sort
puts result.inspect   # => ["JAVA","PYTHON","RUST"]

# группировка по длине
puts words.group_by(&:length).inspect
# => {4=>["ruby","rust","java"], 2=>["go"], 6=>["python"]}

Как работает под капотом

Все методы Enumerable полагаются на один-единственный each. Модуль Enumerable подмешивается (mixin) в Array, Hash, Range и Set, а взамен требует, чтобы класс умел each. Дав классу each и подключив Enumerable, вы бесплатно получаете map, select и всю остальную сотню методов. Это блестящий пример переиспользования кода через модули.

   [1,2,3].map { |n| n*n }
        |
   each отдаёт 1 --> блок n*n --> 1  --+
   each отдаёт 2 --> блок n*n --> 4  --+--> новый массив [1,4,9]
   each отдаёт 3 --> блок n*n --> 9  --+
        |
   reduce(0) {|s,n| s+n }:
   s=0 --> +1 --> s=1 --> +2 --> s=3 --> +3 --> s=6

Та же логика «преобразовать-отфильтровать-свернуть» на Python:

# Та же логика на Python ▶
from functools import reduce
nums = [1, 2, 3, 4, 5, 6]
print([n*n for n in nums])              # map  -> [1,4,9,16,25,36]
print([n for n in nums if n % 2 == 0])  # select -> [2,4,6]
print(reduce(lambda s, n: s + n, nums, 0))  # reduce -> 21

Частые ошибки

  • map вместо each и наоборот. map для трансформации (нужен новый массив), each для побочных эффектов (печать). Перепутали — либо лишний массив, либо потеря результата.
  • Забыть начальное значение reduce. Без него первый элемент становится начальным, что иногда даёт неверный тип результата.
  • Громоздкие блоки. Если блок длинный и сложный, цепочка становится нечитаемой — вынесите логику в метод.

Best practices

  • Предпочитайте Enumerable ручным циклам: map/select/reduce декларативнее и менее подвержены ошибкам.
  • Используйте &:method в простых блоках: names.map(&:strip) вместо names.map { |n| n.strip }.
  • Знайте короткие специализированные методы: sum, min, max, count, any?, all? — они выразительнее, чем reduce вручную.

Глубже: ленивые цепочки и производительность

Когда вы привыкнете к цепочкам Enumerable, возникнет естественный вопрос о производительности: ведь каждый map и select в цепочке создаёт промежуточный массив. Для небольших коллекций это незаметно, но на больших данных промежуточные массивы могут стать проблемой. Здесь на помощь приходит lazy: добавив его в начало цепочки, вы превращаете её в ленивую — элементы протекают сквозь все шаги по одному, без создания промежуточных массивов, а вычисление останавливается, как только получен нужный результат. Это особенно ценно в связке с first(n) или take(n). Второй важный аспект — выбор правильного метода вместо универсального reduce: для суммы есть sum, для подсчёта по условию — count { }, для проверки «хоть один» и «все» — any? и all?, для частот — tally. Специализированные методы не только короче, но и быстрее, потому что реализованы на C и не строят лишних структур. Освоение Enumerable — это не разовое заучивание, а постепенное накопление словаря: каждый новый метод делает ваши преобразования данных чуть выразительнее и эффективнее.

Итог. Enumerable — модуль, дающий коллекциям map, select, reduce и десятки других методов поверх единственного each. Цепочки этих методов заменяют ручные циклы и делают код декларативным. Это ядро повседневного Ruby.

Проверьте себя
1. Какой метод Enumerable нужен, чтобы получить новый массив, преобразовав каждый элемент?
Aeach
Bselect
Cmap
Dreduce
2. На каком единственном методе построены все возможности модуля Enumerable?
Amap
Beach
Cto_a
Dsort