Lesson 4 / 25

Conditionals, Loops and for Comprehensions

Use if, while and for comprehensions with generators, guards and yield.

Control flow the Scala way

Scala 3 supports the new optional-brace syntax: if x > 0 then ... else ..., while cond do ... and for x <- xs do ..., with blocks marked by indentation (braces still work). The for comprehension is more than a loop. It has generators (x <- xs), guards (if x > 0) and definitions (y = x * 2), and with yield it builds a new collection: for o <- orders if o.paid yield o.total. Multiple generators nest like nested loops. Behind the scenes, the compiler translates for into calls to map, flatMap, withFilter and foreach, which is why for comprehensions also work with Option, Either, Future and effect types: a sequence of steps that may fail or run asynchronously reads like straight-line code. while loops exist for low-level, performance-sensitive code, but idiomatic Scala usually prefers collection operations or recursion. There is no break keyword; use scala.util.boundary and break from the standard library, or restructure with find, takeWhile or exists.

How for comprehensions are translated

Generators become flatMap and map, and guards become withFilter.

A for block on the left with arrows pointing to a chain of method boxes on the right labelled flatMap, withFilter and map.
Figure 2.1 — for comprehensions desugar into method calls.

for comprehensions with lists and Options

The same syntax works for collections and for values that may be missing.

case class Order(id: String, city: String, items: List[Item], paid: Boolean)
case class Item(sku: String, qty: Int, pricePaise: Int)

val orders = List(
  Order("o1", "Pune", List(Item("pen", 2, 4950), Item("ink", 1, 19900)), paid = true),
  Order("o2", "Delhi", List(Item("pad", 5, 9900)), paid = false)
)

val paidLines =
  for
    o <- orders if o.paid
    item <- o.items
    lineTotal = item.qty * item.pricePaise
  yield s"${o.id} ${item.sku}: $lineTotal"
// List(o1 pen: 9900, o1 ink: 19900)

val stock = Map("pen" -> 120, "ink" -> 0)
val prices = Map("pen" -> 4950, "ink" -> 19900)

def stockValue(sku: String): Option[Int] =
  for
    units <- stock.get(sku)            // None stops the chain
    price <- prices.get(sku)
  yield units * price

println(stockValue("pen"))             // Some(594000)
println(stockValue("pad"))             // None

var i = 0
while i < 3 do
  println(s"tick $i")
  i += 1

A recipe that works in any kitchen

A for comprehension is a recipe written in steps. Whether the kitchen is a list (many results), an Option (maybe no result) or a Future (result later), each kitchen knows how to follow the same steps.

Quick check: What does the compiler translate a for comprehension with yield into?

  • A while loop
  • Java bytecode directly with no method calls
  • Calls to map, flatMap and withFilter
  • Recursive functions only
Answer

Calls to map, flatMap and withFilter — Generators and guards desugar into flatMap, map and withFilter, so any type with those methods works.