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.