Что такое 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, и позволяет использовать единый синтаксис для работы с самыми разными типами — коллекциями, опциями, будущими значениями и даже ошибками.