map и filter: преобразование данных

Две операции — map и filter — заменяют большинство циклов и делают код похожим на описание намерения.

«Не „как пройти по списку“, а „что я хочу получить“ — вот разница между циклом и map.»

map применяет функцию к каждому элементу и собирает результаты в новую коллекцию. filter оставляет только те элементы, для которых функция вернула true. Оба — функции высшего порядка, оба не меняют исходную коллекцию.

val nums = List(1, 2, 3, 4, 5)

val doubled = nums.map(x => x * 2)      // List(2, 4, 6, 8, 10)
val evens = nums.filter(x => x % 2 == 0) // List(2, 4)
val squares = nums.map(x => x * x)       // List(1, 4, 9, 16, 25)

Можно использовать краткую запись с _: nums.map(_ * 2).

Цепочки операций

Сила в том, что операции соединяются в цепочку, и каждая описывает один шаг преобразования.

val nums = List(1, 2, 3, 4, 5, 6)
val result = nums
  .filter(_ % 2 == 0)   // оставить чётные -> 2, 4, 6
  .map(_ * 10)          // умножить на 10 -> 20, 40, 60
println(result)         // List(20, 40, 60)

Сравнение с обычным циклом

ЦИКЛ (как):                  MAP/FILTER (что):
  res = []                     nums
  for x in nums:                 .filter(_ % 2 == 0)
    if x % 2 == 0:               .map(_ * 10)
      res.add(x * 10)
  -> вручную и многословно     -> декларативно и кратко

map на Map и Set

val ages = Map("Аня" -> 25, "Иван" -> 30)
val older = ages.map((name, age) => (name, age + 1))
println(older)   // Map(Аня -> 26, Иван -> 31)

Та же идея на Python ▶

# map и filter на Python
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))     # [2,4,6,8,10]
evens = list(filter(lambda x: x % 2 == 0, nums))  # [2,4]

# идиоматичнее — генераторы списков
doubled = [x * 2 for x in nums]
evens = [x for x in nums if x % 2 == 0]

# цепочка: чётные, затем *10
result = [x * 10 for x in nums if x % 2 == 0]
print(result)   # [20, 40]

Как работает под капотом (JVM)

map и filter внутри обходят коллекцию и строят новую через builder. Функция, которую вы передаёте, — это объект Function1 с методом apply, вызываемый для каждого элемента (как мы видели в разделе про функции). Для List результат — новый связный список. Важно: каждая операция в цепочке создаёт промежуточную коллекцию. Для больших данных это бывает накладно — тогда используют view или LazyList, чтобы вычислять лениво и не создавать промежутки.

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

  • Путать map и foreach. map собирает результаты, foreach только выполняет действие и возвращает Unit.
  • Побочные эффекты внутри map. map для преобразования, а не для печати; для эффектов есть foreach.
  • Длинные цепочки на огромных коллекциях. Множество промежуточных коллекций — повод задуматься о view.

Best practices

  • Предпочитайте map/filter ручным циклам — код читается как описание задачи.
  • Соединяйте операции в цепочки, каждая — один понятный шаг.
  • Для тяжёлых данных рассмотрите ленивые view, чтобы избежать промежуточных коллекций.

От «как» к «что»: сдвиг мышления

Переход от циклов к map и filter — это не просто другой синтаксис, а другой способ думать о задаче. Цикл описывает механику: заведи аккумулятор, пройди по индексам, на каждом шаге проверь и добавь. map и filter описывают намерение: «преобразуй каждый элемент так», «оставь подходящие под это условие». Читатель кода сразу видит цель, а не реконструирует её из деталей итерации.

Цепочки операций усиливают этот эффект. Каждое звено выполняет один понятный шаг, и вся трансформация читается сверху вниз как рецепт. Это резко снижает количество ошибок: в цикле легко перепутать границы или забыть инициализацию, а в декларативной цепочке каждый шаг изолирован и проверяем. Когда данные большие, на помощь приходят ленивые представления, которые позволяют сохранить ту же читаемость без создания промежуточных коллекций.

Маленькая, но важная привычка — выбирать правильную операцию по намерению: map, когда нужно преобразовать каждый элемент с сохранением количества; filter, когда нужно отобрать; foreach, когда нужен только побочный эффект. Чёткое разделение этих ролей делает цепочки преобразований предсказуемыми, а код — понятным с первого взгляда.

Итоги. map преобразует каждый элемент, filter отбирает по условию; они соединяются в декларативные цепочки и не меняют исходные данные. Дальше — свёртка через fold и reduce.

Проверьте себя
1. Что делает map?
AУдаляет элементы по условию
BПрименяет функцию к каждому элементу и собирает результаты в новую коллекцию
CСортирует коллекцию
DСчитает сумму
2. В чём разница между map и foreach?
AНет разницы
Bmap собирает результаты в новую коллекцию, а foreach выполняет действие и возвращает Unit
Cforeach быстрее всегда
Dmap работает только с числами