For Comprehensions
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, 2026-10-12) and every example was run with Scala CLI 1.17.1 and Scala 3.10.0 on
JDK 26 (checked on 2026-10-15). Features newer than the 3.3 LTS name the release that brought them, and say whether they work
on the 3.9 LTS. A for in Scala is not a language-level loop. It is syntax that the compiler rewrites into calls to
foreach, map, flatMap and withFilter on whatever value follows the <-. That is why the same construct iterates a
List, chains Option lookups, sequences Future computations and short-circuits on the first Left of an Either. This
page covers the syntax, the rewriting, what a type must provide, and the 3.4 and 3.8 changes. The plain while and if
forms are on Conditionals & Loops, patterns are on
Pattern Matching and
Extractors & Pattern Bindings, and the monadic types
used in the later sections are on Option & Null Safety,
Either & Try and
Futures & Promises. For comparison, Java’s loops are on
Java Control Flow, Kotlin’s on
Kotlin Control Flow and Python’s comprehensions on
Python Iterators, Generators & Comprehensions.
Loops, generators, guards and definitions
A for expression is built from enumerators and a body. A generator has the form pattern <- expression and draws
elements from the expression. A guard is an if followed by a Boolean condition and skips the elements for which it is
false. A value definition name = expression binds a name inside the comprehension. With do as the body the construct is a
loop that runs for its side effects and has type Unit. With yield it is a comprehension that builds a result.
//> using scala 3.10.0
enum Difficulty:
case Easy, Moderate, Hard
case class Trail(name: String, km: Double, ascent: Int, difficulty: Difficulty)
@main def loops(): Unit =
val trails = List(
Trail("Lakeside", 4.5, 120, Difficulty.Easy),
Trail("Ridge", 11.0, 900, Difficulty.Hard),
Trail("Forest", 7.2, 300, Difficulty.Moderate)
)
// a loop: foreach, result is Unit
for t <- trails do println(t.name)
// guard, several generators and a definition
for
t <- trails
if t.difficulty != Difficulty.Easy
i <- 1 to 2
label = s"${t.name}#$i"
do println(label)
// yield builds a new collection of the same kind as the first generator
val names = for t <- trails if t.ascent > 200 yield t.name.toUpperCase
println(names)
val pairs = for
a <- List(1, 2)
b <- Vector("x", "y")
yield (a, b)
println(pairs)
val set = for t <- trails.toSet yield t.difficulty
println(set)
assert(names == List("RIDGE", "FOREST"))
Lakeside
Ridge
Forest
Ridge#1
Ridge#2
Forest#1
Forest#2
List(RIDGE, FOREST)
List((1,x), (1,y), (2,x), (2,y))
Set(Easy, Hard, Moderate)
Three points are worth keeping in mind.
-
The result of a comprehension takes its type from the first generator. Iterating a
Listgives aList, iterating aSetgives aSet(so duplicates collapse), iterating anOptiongives anOption. In thepairsexample the second generator is aVector, yet the result is aList, because the first one is. -
The enumerators can be written one per line, as above, or on one line separated by semicolons, and a single enumerator can follow the
fordirectly (for t <- trails do ...). The Scala 2 spelling with parentheses or braces (for (t <- trails) ...,for { t <- trails; if ... } yield t) is still accepted, and in a braceless block the line breaks separate enumerators with no punctuation. -
dois the keyword that ends the enumerators of a loop andyieldis the one for a comprehension. A body made of several statements goes on indented lines afterdooryield.
Source: Scala 3 Book: for loops, multiple generators, guards and for expressions
A few more shapes, all run on 3.10.0:
//> using scala 3.10.0
@main def shapes(): Unit =
val hut: Option[Int] = Some(12)
// Scala 2 syntax with braces and parentheses still works
val old = for {
n <- List(1, 2, 3)
if n % 2 == 1
} yield n * 10
println(old)
// a Map: the pattern takes the (key, value) pair apart
val m = Map("Ridge" -> 11.0, "Forest" -> 7.2)
val long = for (name, km) <- m if km > 10 yield name
println(long)
// a for loop is only foreach; its value is Unit
val u: Unit = for n <- 1 to 3 do print(n)
println()
println(u)
// the first generator decides the result type
val opt = for
b <- hut
n <- List(1, 2).headOption
yield n * b
println(opt)
println(for c <- "abc" yield c.toUpper)
println(for c <- "abc" yield c.toInt)
assert(old == List(10, 30))
List(10, 30)
List(Ridge)
123
()
Some(12)
ABC
ArraySeq(97, 98, 99)
Iterating a Map with one variable in the pattern per component, or a String, works because the compiler only needs the
rewriting to type-check (a String is converted to StringOps, whose map builds a String when the function returns a
Char, and an ArraySeq otherwise). A for ... do over a Range such as 1 to 3 is a foreach call; the range is not
materialized.
What the compiler generates
The rewriting is mechanical. Each generator becomes a method call on its expression, and the rest of the comprehension becomes the function that is passed to it.
The compiler can show the result. The option -Vprint:typer prints the typed tree. This comprehension has two generators, a
guard and a definition:
//> using scala 3.10.0
//> using option -Vprint:typer
@main def x(): Unit =
val r = for
a <- List(1, 2)
b <- List(3, 4)
if a < b
c = a * b
yield (a, c)
println(r)
The relevant part of the compiler’s output, reformatted and with the colour codes removed:
val r: List[(Int, Int)] =
List.apply[Int](1, 2).flatMap[(Int, Int)]((a: Int) =>
List.apply[Int](3, 4).withFilter((b: Int) => a < b).map[(Int, Int)]((b: Int) =>
{
val c: Int = a * b
Tuple2.apply[Int, Int](a, c)
}
)
)
Source: Scala 3 Book: for expressions
So the rules are the following.
-
Every generator except the last one becomes
flatMap, whose function must return a collection (or another container with the same operations). The last generator becomesmapwhen the body isyield, andforeachwhen it isdo. In a loop with several generators, every generator becomes aforeach. -
A guard becomes
withFilterapplied to the generator before it.withFilterrather thanfilteris deliberate: for collections it returns a lightweight view object that does no work until the followingmaporflatMap, so the collection is not traversed twice. -
A definition becomes a
valat the start of the function body that receives the next elements. -
A pattern that is not a plain name is the parameter pattern of that function, and a pattern that can fail adds a
withFilter(see Refutable patterns andcase).
You do not have to take this on trust, because a type can log the calls the compiler makes:
//> using scala 3.10.0
/** A box that reports which method the compiler calls. */
case class Box[A](value: A):
def map[B](f: A => B): Box[B] =
println("map")
Box(f(value))
def flatMap[B](f: A => Box[B]): Box[B] =
println("flatMap")
f(value)
def withFilter(p: A => Boolean): Box[A] =
println("withFilter")
this
def foreach(f: A => Unit): Unit =
println("foreach")
f(value)
@main def desugar(): Unit =
println("-- two generators and yield")
val r1 = for
a <- Box(1)
b <- Box(2)
yield a + b
println(r1)
println("-- guard")
val r2 = for
a <- Box(1)
if a > 0
yield a
println(r2)
println("-- loop")
for a <- Box(1) do println(a)
-- two generators and yield
flatMap
map
Box(3)
-- guard
withFilter
map
Box(1)
-- loop
foreach
1
What a type needs to support for
Nothing is inherited from a trait. The compiler only checks that the calls type-check, so the requirement is the set of methods the comprehension uses.
| The comprehension uses | The type needs |
|---|---|
|
|
|
|
|
|
|
|
|
|
The standard library types provide all four methods, so the whole table applies to collections, Option, Either (right
biased), Try, Future and Iterator. Your own types can opt in by declaring the methods; they do not need to extend any
common type. Leaving one out fails at the use site. This Wrapper has map and flatMap only:
//> using scala 3.10.0
case class Wrapper[A](value: A):
def map[B](f: A => B): Wrapper[B] = Wrapper(f(value))
def flatMap[B](f: A => Wrapper[B]): Wrapper[B] = f(value)
@main def missing(): Unit =
val ok = for
a <- Wrapper(1)
b <- Wrapper(2)
yield a + b
println(ok)
val bad = for
a <- Wrapper(1)
if a > 0
yield a
println(bad)
for a <- Wrapper(1) do println(a)
[error] ./f.scala:15:10
[error] value withFilter is not a member of Wrapper[Int]
[error] a <- Wrapper(1)
[error] ^^^^^^^^^^
[error] ./f.scala:20:12
[error] value foreach is not a member of Wrapper[Int]
[error] for a <- Wrapper(1) do println(a)
[error] ^^^^^^^^^^
Source: Scala 3 Book: for expressions and Scala API: WithFilter
The first comprehension compiles (flatMap and map are enough) and the other two do not. The error names the missing
method and points at the generator expression. Types that model a sequence of steps, such as a state action or a Writer, often provide map and
flatMap and leave out withFilter on purpose, because they have no natural empty value to return when a guard fails. See
Type Classes for how libraries abstract over such types.
Mixing container types is possible when the outer flatMap accepts the inner type. Inside a comprehension over a List, an
Option generator is accepted because, since Scala 2.13, Option is itself an IterableOnce, which List.flatMap takes. The reverse does not work, since Option.flatMap
must return an Option:
//> using scala 3.10.0
@main def mixing(): Unit =
val hut: Option[Int] = Some(12)
val bad = for
b <- hut
n <- List(1, 2)
yield n * b
println(bad)
[error] ./j.scala:7:5
[error] Found: List[Int]
[error] Required: Option[Any]
Convert explicitly, for example with hut.toList or List(1, 2).headOption, so that the first generator and the rest agree.
Source: Scala 3 Book: for expressions
Better fors (SIP-62, stable in 3.8)
The classic rewriting had two surprises. First, a value definition name = expression could not be the first enumerator, and
a definition that followed a generator made the compiler pair the generated value with it in a tuple through an extra
map. Second, the last generator was always followed by a map even when the body simply returned the generator’s own
value, which cost an extra call to map.
Better fors (SIP-62) improves both. It was previewed in 3.7 and has been stable and
on by default since Scala 3.8 (announced on 2026-01-22), so it applies to 3.10 and to the 3.9 LTS. It does not exist in the
3.3 LTS. The same Box type shows the effect. This program has three comprehensions that use aliases:
//> using scala 3.10.0
case class Box[A](value: A):
def map[B](f: A => B): Box[B] =
println("map")
Box(f(value))
def flatMap[B](f: A => Box[B]): Box[B] =
println("flatMap")
f(value)
@main def betterFors(): Unit =
println("-- alias after a generator")
val r3 = for
a <- Box(1)
b = a + 1
c <- Box(b)
yield c
println(r3)
println("-- alias before the first generator (3.8+)")
val r4 = for
k = 10
a <- Box(k)
yield a
println(r4)
println("-- trailing alias then yield")
val r5 = for
a <- Box(1)
b = a + 1
yield b
println(r5)
Compiled normally on 3.10.0:
-- alias after a generator
flatMap
Box(2)
-- alias before the first generator (3.8+)
Box(10)
-- trailing alias then yield
map
Box(2)
Compiled with -source:3.7 (the pre-3.8 desugaring), the first and third comprehensions need more calls, and the second one
does not compile at all:
-- alias after a generator
map
flatMap
map
Box(2)
-- trailing alias then yield
map
map
Box(2)
[error] ./b.scala:46:7
[error] '<-' expected, but '=' found
[error] k = 10
[error] ^
Reading the two outputs side by side gives the rules of the new desugaring.
-
Aliases before the first generator are allowed (
k = 10above). The alias is avalevaluated once before the firstflatMap,maporforeach. -
An alias no longer produces an intermediate tuple. The old rewriting called
mapto build(a, b)and thenflatMapover the pair, which is the extra leadingmapin the 3.7 output. -
The final identity
mapis dropped. When a comprehension ends withc <- Box(b)and yields justc, the last generator is the result and the compiler does not wrap it in a redundantmap(c => c). A comprehension such asr5, whoseyieldis not just the generator variable, still callsmaponce.
The difference in behaviour is visible only for types that observe their calls (a logging type, a lazy computation, a type
whose map is not free), but the generated code is also smaller.
Source: Scala 3 Reference: Better fors, SIP-62 and Scala 3.8 announcement
Refutable patterns and case
The left side of <- is a pattern. If the pattern always matches (a name, a tuple of names, a case-class pattern on a value
of exactly that type) it is irrefutable and is used as the parameter pattern directly. If it can fail, Scala 3 asks you to
say so with the case keyword, which then filters the elements that do not match. Without case, a pattern that is
narrower than the type of the generator’s elements is rejected:
//> using scala 3.10.0
@main def patterns(): Unit =
val xs: List[Option[Int]] = List(Some(1), None, Some(3))
// Some(n) is refutable: the generator needs a filter
val a = for Some(n) <- xs yield n
println(a)
[error] ./c.scala:7:15
[error] pattern's type Some[Int] is more specialized than the right hand side expression's type Option[Int]
[error]
[error] If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern,
[error] which will result in a filtering for expression (using `withFilter`).
[error] This patch can be rewritten automatically under -rewrite -source 3.2-migration.
[error] val a = for Some(n) <- xs yield n
[error] ^^^^^^^
Adding case fixes it, and it also works for tuples and other patterns with a guard:
//> using scala 3.10.0
@main def patterns(): Unit =
val xs: List[Option[Int]] = List(Some(1), None, Some(3))
val a = for case Some(n) <- xs yield n
println(a)
val pairs = List((1, "one"), (2, "two"))
val b = for (n, s) <- pairs yield s * n
println(b)
val c = for case (n, s) <- pairs if n > 1 yield s
println(c)
List(1, 3)
List(one, twotwo)
List(two)
(n, s) against a List[(Int, String)] is irrefutable, so no case is needed. Some(n) against Option[Int] is
refutable. The case version is exactly a withFilter plus a map with a partial-function-style match:
//> using scala 3.10.0
@main def tupleDesugar(): Unit =
val pairs = List((1, "one"), (2, "two"))
val viaCase = for case (n, "two") <- pairs yield n
println(viaCase)
val manual = pairs.withFilter {
case (n, "two") => true
case _ => false
}.map { case (n, _) => n }
println(manual)
assert(viaCase == manual)
List(2)
List(2)
The rule is announced in the 3.4 release. Under -source:3.3 the 3.10 compiler downgrades the error above to a warning with
the same text and keeps the old behaviour, which silently dropped the non-matching elements, so code written for the 3.3 LTS
compiles and prints List(1, 3). With the default source of 3.10 the error applies. Scala 2 never
asked for case; every generator pattern filtered silently, so a typo in the pattern removed elements without a diagnostic.
For over Option, Either and Future
Because the rewriting only needs flatMap and map, the same syntax sequences computations that may fail or finish later.
The trailhead planner looks up a trail and then its hut; Either carries a message on failure; Future sequences two
asynchronous calls.
//> using scala 3.10.0
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
case class Trail(name: String, km: Double)
val trails = Map("Ridge" -> Trail("Ridge", 11.0), "Forest" -> Trail("Forest", 7.2))
val hutBeds = Map("Ridge" -> 12)
def find(name: String): Option[Trail] = trails.get(name)
def parseKm(s: String): Either[String, Double] =
s.toDoubleOption.toRight(s"not a number: $s")
def forecast(name: String): Future[Int] = Future(name.length)
@main def monads(): Unit =
val beds = for
t <- find("Ridge")
b <- hutBeds.get(t.name)
yield s"${t.name}: $b beds"
println(beds)
println(for t <- find("Forest"); b <- hutBeds.get(t.name) yield b)
val total = for
a <- parseKm("4.5")
b <- parseKm("7.0")
yield a + b
println(total)
println(for a <- parseKm("4.5"); b <- parseKm("x") yield a + b)
val f = for
d1 <- forecast("Ridge")
d2 <- forecast("Forest")
yield d1 + d2
println(Await.result(f, 5.seconds))
Some(Ridge: 12 beds)
None
Right(11.5)
Left(not a number: x)
11
Each of the five comprehensions stops at the first empty or failed step: None when the Forest trail has no hut, the first
Left when parsing fails. A Future comprehension is sequential by construction. The second forecast call is created only
after the first one finished, because it sits inside the function passed to flatMap. To run independent futures
concurrently, create them before the comprehension (val f1 = forecast("Ridge"), val f2 = forecast("Forest")) and then
combine them in the for. Await.result is used here only to print in a demo program. Real code would compose futures and
avoid blocking; see Futures & Promises.
Source: Scala 3 Book: for expressions
Pitfalls and differences
-
Guards and intermediate collections. A guard in the
forbecomeswithFilterand is fused with the followingmaporflatMap. Writing the same condition as a separate.filter(...)call before theforbuilds an intermediate collection. -
The first generator sets the result type. Putting the
Optiongenerator first in aListpipeline is a compile error (shown above), and putting aSetfirst removes duplicates from the result. -
A comprehension is not a loop.
for x <- xs yield f(x)callsmap; usedofor side effects. Writingyieldand then ignoring the result builds a collection that is thrown away. -
Early exit. There is no
breakorcontinuein afor. A guard skips an element (continue), andbreak-like behaviour is covered on Exceptions, boundary & Early Exit. -
Nested calls versus nesting
for. Several generators in oneforproduce a single flat result; two nestedfor ... yieldexpressions produce a collection of collections.
How other languages compare:
-
Java has an enhanced
for(for (var t : trails)) that is a statement with no value. The nearest equivalent of a comprehension is aStreampipeline withflatMap,filterandmap, which is eager only at the terminal operation. -
Kotlin has
forloops over iterables and ranges and no comprehension syntax. Sequences andflatMapfill that role, and coroutine builders such assequence { }offer generator-like code. -
Python has list, set and dict comprehensions with
forandifclauses. They are always eager for lists and sets, lazy only for generator expressions, and cover one container kind rather than any type withflatMap. -
Scala 2 has the same rewriting, braces and parentheses, no alias before the first generator, no
doafter the enumerators (it writesfor (...) body), silent filtering of refutable patterns and the extra tuplemapfor aliases.scalac 2.13accepts the classic form; the changes above are Scala 3 only.
Where the books are outdated
The Scala 3 Book’s section on for and the Tour describe the classic rewriting and the if guard, which are still correct.
They predate two things covered above: aliases before the first generator and the shorter desugaring (3.8), and the
case keyword requirement for refutable patterns, which dates from 3.4 and is an error in current releases. Older printed
books written for Scala 2 show for { ... } yield with braces and rely on silent pattern filtering. The braces still compile
in Scala 3. The silent filtering does not.