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 List gives a List, iterating a Set gives a Set (so duplicates collapse), iterating an Option gives an Option. In the pairs example the second generator is a Vector, yet the result is a List, 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 for directly (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.

  • do is the keyword that ends the enumerators of a loop and yield is the one for a comprehension. A body made of several statements goes on indented lines after do or yield.

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.

flowchart TD F["for enumerators ... body"] --> Y{"body is yield?"} Y -->|"no, do"| FE["generators use foreach"] Y -->|"yes"| LG{"last generator?"} LG -->|"yes"| M["xs.map with the yield expression"] LG -->|"no, more follow"| FM["xs.flatMap with the rest as a function returning a collection"] F --> G{"guard: if cond"} G --> W["xs.withFilter(cond) before the next call"] F --> D{"definition: x = e"} D --> V["val x = e inside the function body"] F --> P{"pattern is refutable?"} P -->|"yes, needs case"| W2["xs.withFilter with a match that tests the pattern"] P -->|"no"| X["pattern becomes the parameter of the function"]

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)
      }
    )
  )

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 becomes map when the body is yield, and foreach when it is do. In a loop with several generators, every generator becomes a foreach.

  • A guard becomes withFilter applied to the generator before it. withFilter rather than filter is deliberate: for collections it returns a lightweight view object that does no work until the following map or flatMap, so the collection is not traversed twice.

  • A definition becomes a val at 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 and case).

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

for x <- xs do ...

foreach(f: A => U)

for x <- xs yield e

map(f: A => B)

for x <- xs; y <- ys yield e

flatMap(f: A => M[B]) on xs, and map on ys

for x <- xs if p yield e

withFilter(p: A => Boolean) and map

for case pattern <- xs ...

withFilter, then the operation above

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]            ^^^^^^^^^^

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.

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 = 10 above). The alias is a val evaluated once before the first flatMap, map or foreach.

  • An alias no longer produces an intermediate tuple. The old rewriting called map to build (a, b) and then flatMap over the pair, which is the extra leading map in the 3.7 output.

  • The final identity map is dropped. When a comprehension ends with c <- Box(b) and yields just c, the last generator is the result and the compiler does not wrap it in a redundant map(c => c). A comprehension such as r5, whose yield is not just the generator variable, still calls map once.

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.

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.

Pitfalls and differences

  • Guards and intermediate collections. A guard in the for becomes withFilter and is fused with the following map or flatMap. Writing the same condition as a separate .filter(...) call before the for builds an intermediate collection.

  • The first generator sets the result type. Putting the Option generator first in a List pipeline is a compile error (shown above), and putting a Set first removes duplicates from the result.

  • A comprehension is not a loop. for x <- xs yield f(x) calls map; use do for side effects. Writing yield and then ignoring the result builds a collection that is thrown away.

  • Early exit. There is no break or continue in a for. A guard skips an element (continue), and break-like behaviour is covered on Exceptions, boundary & Early Exit.

  • Nested calls versus nesting for. Several generators in one for produce a single flat result; two nested for ... yield expressions 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 a Stream pipeline with flatMap, filter and map, which is eager only at the terminal operation.

  • Kotlin has for loops over iterables and ranges and no comprehension syntax. Sequences and flatMap fill that role, and coroutine builders such as sequence { } offer generator-like code.

  • Python has list, set and dict comprehensions with for and if clauses. They are always eager for lists and sets, lazy only for generator expressions, and cover one container kind rather than any type with flatMap.

  • Scala 2 has the same rewriting, braces and parentheses, no alias before the first generator, no do after the enumerators (it writes for (...) body), silent filtering of refutable patterns and the extra tuple map for aliases. scalac 2.13 accepts 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.