Scala's
for/yieldis sugar overflatMap,map, andwithFilter. Once you see the desugaring, you understand howforworks for anything with those methods — Options, Eithers, Futures, IOs, even your own types.
In this chapter:
- The simple cases
- Desugaring
- Generators, guards, value definitions
foroverOption,Either,Try,Future- Pattern matching in generators
- Building your own monad that works in
for - Common pitfalls
Iterate and produce:
val squares = for (i <- 1 to 5) yield i * i
// Vector(1, 4, 9, 16, 25)Multiple generators (Cartesian product):
for {
x <- 1 to 3
y <- 1 to 3
} yield (x, y)
// Vector((1,1), (1,2), (1,3), (2,1), (2,2), (2,3), (3,1), (3,2), (3,3))With a guard:
for {
x <- 1 to 10
if x % 2 == 0
} yield x * x
// Vector(4, 16, 36, 64, 100)Without yield (side-effecting loop):
for (x <- List(1, 2, 3)) println(x)Every for desugars to chains of flatMap, map, and withFilter. Three rules:
Rule 1: a single generator with yield becomes map.
for (x <- xs) yield f(x)
// desugars to:
xs.map(x => f(x))Rule 2: multiple generators become nested flatMap + a final map.
for {
x <- xs
y <- ys
} yield (x, y)
// desugars to:
xs.flatMap(x => ys.map(y => (x, y)))Rule 3: guards become withFilter.
for {
x <- xs
if x > 0
} yield x
// desugars to:
xs.withFilter(x => x > 0).map(x => x)So this:
for {
x <- xs
y <- ys
if pred(x, y)
} yield combine(x, y)becomes:
xs.flatMap { x =>
ys.withFilter(y => pred(x, y))
.map(y => combine(x, y))
}This desugaring is why for works on any type that has map, flatMap, and withFilter. The compiler doesn't know it's a List — it just sees that the methods exist.
Three constructs in a for:
for {
x <- xs // generator
if x > 0 // guard
y = x * 2 // value definition (no <- )
z <- 1 to y // another generator
} yield (x, y, z)Generator (x <- xs) — pulls one value out of a "container" (like flatMap).
Guard (if cond) — filters; non-matching elements are skipped.
Value definition (y = x * 2) — a regular val, scoped through the rest of the comprehension. No <-.
Because all of these have flatMap and map, for works on them transparently:
def parseInt(s: String): Option[Int] = s.toIntOption
val sum: Option[Int] = for {
a <- parseInt("10")
b <- parseInt("20")
c <- parseInt("30")
} yield a + b + c
// Some(60)
val bad: Option[Int] = for {
a <- parseInt("10")
b <- parseInt("xyz") // None — short-circuits
c <- parseInt("30") // never runs
} yield a + b + c
// Nonedef parseInt(s: String): Either[String, Int] =
s.toIntOption.toRight(s"not an int: $s")
val sum: Either[String, Int] = for {
a <- parseInt("10")
b <- parseInt("20")
} yield a + b
// Right(30)
val bad = for {
a <- parseInt("10")
b <- parseInt("xyz") // Left, short-circuits
} yield a + b
// Left("not an int: xyz")import scala.util.Try
val result: Try[Int] = for {
a <- Try("10".toInt)
b <- Try("20".toInt)
} yield a + b
// Success(30)import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
def fetch(url: String): Future[String] = ???
val combined: Future[String] = for {
a <- fetch("/a")
b <- fetch("/b") // sequential — runs after a completes
c <- fetch("/c")
} yield s"$a / $b / $c"Note: for on Futures runs them sequentially — each one waits for the previous. For parallel, kick them off first:
val fa = fetch("/a")
val fb = fetch("/b")
val fc = fetch("/c")
val combined = for { a <- fa; b <- fb; c <- fc } yield s"$a / $b / $c"
// all three run in parallel; the for waits on each in orderThe left side of <- can be a pattern:
val pairs = List((1, "a"), (2, "b"), (3, "c"))
for {
(n, s) <- pairs
} yield s"$n:$s"
// List("1:a", "2:b", "3:c")If the pattern is partial (might not match), non-matching elements are skipped — same as a guard:
val mixed: List[Any] = List(1, "two", 3, "four", 5)
for {
case i: Int <- mixed // Scala 3 syntax; Scala 2 omits `case`
} yield i * 10
// List(10, 30, 50)Scala 3 note: Refutable patterns require
casekeyword. Scala 2 didn't.
This is one reason for is useful beyond simple iteration — it handles "filter and transform" elegantly.
Any type with flatMap, map, and (optionally) withFilter becomes for-comprehensible. Build your own:
case class Wrapped[A](value: A) {
def map[B](f: A => B): Wrapped[B] = Wrapped(f(value))
def flatMap[B](f: A => Wrapped[B]): Wrapped[B] = f(value)
def withFilter(p: A => Boolean): Wrapped[A] =
if (p(value)) this
else throw new NoSuchElementException("filter failed")
}
val r = for {
a <- Wrapped(10)
b <- Wrapped(20)
} yield a + b
// Wrapped(30)This is exactly how Option, Either, Future, IO, and any user-defined monad earn their for/yield support.
For real monads (with proper laws), see Chapter 17 — Cats's Monad type class formalizes this.
The compiler infers for based on the first generator's type. You can't mix Option and Either in one for:
def maybe: Option[Int] = Some(1)
def err: Either[String, Int] = Right(2)
for {
a <- maybe // Option[Int]
b <- err // Either[String, Int] — type mismatch!
} yield a + bThe fix: convert to the same shape first.
for {
a <- maybe.toRight("missing")
b <- err
} yield a + b
// Either[String, Int]As shown above, for over Futures is sequential. If you mean parallel, kick off the futures before the for:
// sequential
val seq = for {
a <- fetch("/a")
b <- fetch("/b")
} yield (a, b)
// parallel
val fa = fetch("/a")
val fb = fetch("/b")
val par = for { a <- fa; b <- fb } yield (a, b)Some types don't define withFilter. For instance, Future doesn't have a useful withFilter (a future "matched" or "didn't match" doesn't really make sense). Guards in for/Future will compile but throw on filter failure — usually not what you want.
This is a bug:
for {
x <- xs
y <- x + 1 // wrong! tries to flatMap on Int (an actual error)
} yield yThe correct form:
for {
x <- xs
y = x + 1 // value definition
} yield y- The desugaring rules:
<-→flatMap,=→ val,if→withFilter, lastyield→map. forworks onOption,Either,Try,Future, and any custom monad.- Patterns can appear on the left of
<-, with refutable patterns acting as filters. - How to make a custom type
for-comprehensible by definingflatMap/map/withFilter. - The "futures-in-a-for-are-sequential" gotcha and its fix.
The next chapter (Chapter 16 — Type Classes) introduces the type class pattern formally.
← Previous: Chapter 14 — Lazy Evaluation | Back to README | Next: Chapter 16 — Type Classes →