Scala for Comprehensions: Complete Guide with Examples

TT

Scala for Comprehensions: Complete Guide with Examples

Scala's for comprehensions are syntactic sugar for nested map, flatMap, and filter calls. They transform what would be deeply nested functional chains into clean, readable expressions. This module explains how they work and when to use them.

Basic for Loop vs for Comprehension

A plain for loop (without yield) is a side-effecting expression:

scala
for (i <- 1 to 5) {
  println(i)
}

A for comprehension uses yield to produce a new collection:

scala
val squares = for (i <- 1 to 5) yield i * i
println(squares)  // Vector(1, 4, 9, 16, 25)

The yield turns the for into an expression that returns a value instead of just running for side effects.

How for Comprehensions Desugar

The Scala compiler translates for comprehensions into method calls. Understanding this translation is key:

scala
// for comprehension
val result = for {
  x <- List(1, 2, 3)
  y <- List(10, 20)
} yield x * y

// Desugars to:
val result2 = List(1, 2, 3).flatMap { x =>
  List(10, 20).map { y =>
    x * y
  }
}

println(result)   // List(10, 20, 20, 40, 30, 60)
println(result2)  // List(10, 20, 20, 40, 30, 60)

The rule: every generator except the last uses flatMap. The last generator uses map. Guards use withFilter.

Guards in for Comprehensions

Use if to add filters:

scala
val evenSquares = for {
  i <- 1 to 10
  if i % 2 == 0
} yield i * i

println(evenSquares)  // Vector(4, 16, 36, 64, 100)

This desugars to:

scala
(1 to 10).withFilter(_ % 2 == 0).map(i => i * i)

for Comprehensions Over Option

The real power of for comprehensions shows when working with Option:

scala
def findUser(id: Int): Option[String] = Map(1 -> "Alice", 2 -> "Bob").get(id)
def findEmail(name: String): Option[String] =
  Map("Alice" -> "alice@example.com").get(name)

// Without for comprehension — nested flatMap
val email1: Option[String] = findUser(1).flatMap(name => findEmail(name))

// With for comprehension — much cleaner
val email2: Option[String] = for {
  name  <- findUser(1)
  email <- findEmail(name)
} yield email

println(email2)  // Some(alice@example.com)

// Short-circuits at the first None
val noEmail: Option[String] = for {
  name  <- findUser(99)   // None — stops here
  email <- findEmail(name)
} yield email

println(noEmail)  // None

If any generator produces None, the entire comprehension returns None — no null checks, no if-else chains.

for Comprehensions Over Either

The same pattern works with Either:

scala
def parseId(s: String): Either[String, Int] =
  s.toIntOption.toRight(s"'$s' is not a valid ID")

def lookupUser(id: Int): Either[String, String] =
  Map(1 -> "Alice").get(id).toRight(s"User $id not found")

def getEmailForIdString(idStr: String): Either[String, String] = for {
  id    <- parseId(idStr)
  user  <- lookupUser(id)
  email <- findEmail(user).toRight(s"No email for $user")
} yield email

println(getEmailForIdString("1"))    // Right(alice@example.com)
println(getEmailForIdString("abc"))  // Left('abc' is not a valid ID)
println(getEmailForIdString("99"))   // Left(User 99 not found)

Variable Definitions in for Comprehensions

You can define intermediate values with = inside a for:

scala
case class Order(id: Int, items: List[String], discount: Double)

val orders = List(
  Order(1, List("apple", "banana"), 0.1),
  Order(2, List("cherry"), 0.0)
)

val summaries = for {
  order <- orders
  itemCount = order.items.length
  total    = itemCount * 10.0 * (1 - order.discount)
} yield s"Order ${order.id}: $itemCount items, total $total"

summaries.foreach(println)
// Order 1: 2 items, total 18.0
// Order 2: 1 items, total 10.0

= something in a for block is a value definition (not a generator) — it doesn't call flatMap or map, it just introduces a named value.

Nested for Comprehensions

for comprehensions can produce flat results from nested structures:

scala
val matrix = List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))

val flattened = for {
  row <- matrix
  elem <- row
  if elem % 2 != 0
} yield elem

println(flattened)  // List(1, 3, 5, 7, 9)

Frequently Asked Questions

Q: Is a Scala for comprehension the same as a for loop in other languages? No — Scala's for with yield is an expression that produces a value, not just a loop. It's closer to list comprehensions in Python or LINQ in C#. Even without yield, the Scala for is translated into foreach calls rather than a native loop construct. The real insight is that for works with any type that has map, flatMap, and withFilter — not just collections, but also Option, Either, Future, and custom types.

Q: What does it mean that for comprehensions work on monads? A monad is any type that provides flatMap (also called bind) and a way to wrap a value (like Some() or Right()). Scala's for comprehension is a general notation for sequencing monadic operations. When you chain Options in a for, you're using the Option monad. When you chain Futures, you're using the Future monad. The for comprehension gives you the same clean syntax regardless of the underlying type.

Q: What's the difference between = and <- in a for comprehension? <- is a generator — it calls flatMap (or map for the last one) and binds the result. It "unwraps" the container. = is a value definition — it's equivalent to val, just available in the for scope. It doesn't call any methods and doesn't affect the sequencing. Use <- when you have an Option[A] or List[A] and want to bind the A. Use = when you want to name an intermediate calculation.


Part of Scala Mastery Course — Module 13 of 22.