Чем отличаются map, flatMap и foreach?

В Scala методы map, flatMap и foreach применяются к коллекциям и объектам, поддерживающим интерфейс Traversable, Option, Future и др. Несмотря на похожий синтаксис, они выполняют разные задачи и имеют разное поведение.

map

Метод map применяет переданную функцию ко всем элементам коллекции и возвращает новую коллекцию с тем же типом-контейнером, но с новыми элементами.

Сигнатура:

def map\[B\](f: A => B): C\[B\]

Пример:

val nums = List(1, 2, 3)
val doubled = nums.map(x => x \* 2)
// doubled: List(2, 4, 6)

Особенности:

  • Сохраняет структуру контейнера (List, Option, Future и т.п.).

  • Преобразует каждый элемент с помощью функции.

flatMap

Метод flatMap похож на map, но ожидается, что переданная функция возвращает вложенный контейнер. flatMap плоско разворачивает результат, то есть убирает один уровень вложенности.

Сигнатура:

def flatMap\[B\](f: A => Iterable\[B\]): Iterable\[B\]

Пример:

val words = List("a b", "c d")
val chars = words.flatMap(\_.split(" "))
// chars: List("a", "b", "c", "d")

Аналог через map:

val mapped = words.map(\_.split(" "))
// mapped: List(Array("a", "b"), Array("c", "d"))  вложенные массивы

Пример с Option:

val someNum: Option\[Int\] = Some(3)
val flatMapped = someNum.flatMap(x => Some(x \* 2))
// flatMapped: Some(6)

Особенности:

  • Используется, когда функция возвращает вложенный контейнер (List[List[T]], Option[Option[T]], и т.п.).

  • Убирает лишний уровень вложенности (flatten + map).

foreach

Метод foreach не возвращает результат, используется для побочных эффектов: вывода в консоль, изменения состояния, логирования и т.д.

Сигнатура:

def foreach\[U\](f: A => U): Unit

Пример:

val names = List("Alice", "Bob", "Charlie")
names.foreach(name => println(s"Привет, $name"))

Особенности:

  • Используется только ради выполнения действия, а не получения новой коллекции.

  • Возвращает Unit, не создаёт новых значений.

Сравнение на практике

Исходные данные:

val data = List(1, 2, 3)

map

val mapped = data.map(x => x \* 10)
// List(10, 20, 30)

flatMap

val flatMapped = data.flatMap(x => List(x, x \* 10))
// List(1, 10, 2, 20, 3, 30)

foreach

data.foreach(x => println(x \* 10))
// Выводит 10, 20, 30 в консоль, но не возвращает коллекцию

Применение в Option

map с Option

val opt = Some(5)
val mapped = opt.map(_ \* 2)
// mapped: Some(10)

flatMap с Option

val opt = Some(5)
val flatMapped = opt.flatMap(x => if (x > 0) Some(x \* 2) else None)
// flatMapped: Some(10)

foreach с Option

Some(5).foreach(x => println(s"Значение: $x"))
// Выводит "Значение: 5", но возвращает Unit

Применение в Future

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
val fut = Future { 10 }
val mapped = fut.map(_ \* 2)
val flatMapped = fut.flatMap(x => Future(x \* 3))
fut.foreach(println)
  • map: преобразует результат асинхронной операции.

  • flatMap: позволяет строить цепочку зависимых Future.

  • foreach: вызывает побочный эффект после выполнения Future.

Когда использовать что?

Метод Назначение Возвращает
map Преобразование каждого элемента Коллекция того же типа
--- --- ---
flatMap Преобразование с разворачиванием вложенности Плоская коллекция
--- --- ---
foreach Выполнение действия без создания новой коллекции Unit (ничего)
--- --- ---

Пример с вложенной структурой

val data = List(Some(1), None, Some(2))
val mapped = data.map(opt => opt.map(_ \* 10))
// List(Some(10), None, Some(20))
val flatMapped = data.flatMap(opt => opt.map(_ \* 10))
// List(10, 20)

flatMap избавился от None, так как Option внутри List и разворачивание удаляет пустые значения.