Что такое for-comprehension и как он работает?

for-comprehension в Scala — это синтаксический сахар для цепочки вызовов map, flatMap, filter и withFilter, который позволяет писать читаемый и декларативный код при работе с коллекциями, Option, Future и другими типами, поддерживающими эти методы.

1. Основы синтаксиса

Базовая форма for-comprehension:

for (x <- xs) yield x \* 2

Это эквивалентно:

xs.map(x => x \* 2)

Если используется несколько генераторов:

for {
x <- List(1, 2)
y <- List(10, 20)
} yield x + y

Эквивалентно:

List(1, 2).flatMap(x => List(10, 20).map(y => x + y))

2. Как он компилируется

Любой for выражение со yield компилируется в последовательность вызовов map и flatMap, а фильтрация (if) — в withFilter.

Пример:

for {
x <- List(1, 2, 3)
if x % 2 == 1
y <- List(x, x \* 10)
} yield y

Компилируется как:

List(1, 2, 3).withFilter(x => x % 2 == 1).flatMap(x =>
List(x, x \* 10).map(y => y)
)

3. Использование с Option

val result = for {
a <- Some(2)
b <- Some(3)
} yield a + b
//  Some(5)
val fail = for {
a <- Some(2)
b <- None
} yield a + b
//  None

Если хотя бы одно значение None, результат всей конструкции — None.

4. Использование с Future

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
val f = for {
a <- Future(2)
b <- Future(3)
} yield a + b

Внутри for, Future значения автоматически оборачиваются в flatMap, позволяя писать асинхронный код как синхронный.

5. Использование с Either

В Scala 2.12+ for-comprehension поддерживает Either:

val res = for {
a <- Right(2)
b <- Right(3)
} yield a + b
//  Right(5)
val fail = for {
a <- Right(2)
b <- Left("Ошибка")
} yield a + b
//  Left("Ошибка")

Работает, потому что Right и Left реализуют flatMap и map.

6. Использование if в for

Фильтрация значений:

for {
x <- List(1, 2, 3, 4)
if x % 2 == 0
} yield x
//  List(2, 4)

Scala преобразует if в withFilter.

7. Побочные эффекты и for

Если не используется yield, то for превращается в foreach:

for (x <- List(1, 2, 3)) println(x)

Компилятор вызывает:

List(1, 2, 3).foreach(x => println(x))

8. Вложенные выражения

Можно использовать вложенные for:

for {
x <- List(1, 2)
y <- List("a", "b")
} yield s"$x$y"

Результат: List("1a", "1b", "2a", "2b")

9. Пример с вложенными условиями и генераторами

val result = for {
x <- List(1, 2, 3)
if x % 2 == 1
y <- List(10, 20)
if y != 10
} yield x \* y

Превращается в:

List(1, 2, 3)
.withFilter(x => x % 2 == 1)
.flatMap(x => List(10, 20)
.withFilter(y => y != 10)
.map(y => x \* y))

10. Работа с нестандартными типами

Типы, совместимые с for, должны иметь:

  • map

  • flatMap

  • withFilter

  • foreach (если yield не используется)

Например, Try, Option, Either, Future, Stream, List, Seq и любые кастомные типы.

11. Использование for для парсинга, IO, монадических операций

for очень удобно использовать для последовательных вычислений с обработкой ошибок, например:

def divide(a: Int, b: Int): Option\[Int\] =
if (b == 0) None else Some(a / b)
val result = for {
x <- Some(10)
y <- Some(2)
z <- divide(x, y)
} yield z

12. Отличие от обычных циклов

  • Это не императивный цикл, а декларативная конструкция;

  • Всегда работает с функциональными контейнерами (Option, List, Future, и т.д.);

  • Нельзя мутировать переменные внутри for, используется yield и новые значения.

13. Вложенные let внутри for

Можно создавать временные переменные:

for {
x <- List(1, 2, 3)
y = x \* 2
} yield y + 1

В Scala 3 это называется bindings.

for-comprehension в Scala позволяет выразительно и лаконично описывать последовательные трансформации и фильтрации в функциональном стиле. Он базируется на методах map, flatMap, filter и withFilter, и позволяет использовать единый синтаксис для работы с самыми разными типами — коллекциями, опциями, будущими значениями и даже ошибками.