Skip to content
AgileLogics
5 min read

For-Comprehensions

Prerequisites: Higher-order functions, Option: no more null

A for ... yield expression is syntax sugar for map, flatMap, and withFilter. That means it works with any type that has those methods — not just collections.

What it expands to

@main def desugar(): Unit =
  val a = for
    x <- List(1, 2)
    y <- List("a", "b")
  yield s"$x$y"

  val b = List(1, 2).flatMap(x => List("a", "b").map(y => s"$x$y"))

  println(a)
  println(a == b)
List(1a, 1b, 2a, 2b)
true

Every <- except the last becomes flatMap; the last becomes map.

With Option

case class User(id: Int, name: String, managerId: Option[Int])

val users = Map(
  1 -> User(1, "Grace", None),
  2 -> User(2, "Ada", Some(1)),
  3 -> User(3, "Linus", Some(9))
)

def managerName(userId: Int): Option[String] =
  for
    user      <- users.get(userId)
    managerId <- user.managerId
    manager   <- users.get(managerId)
  yield manager.name

@main def options(): Unit =
  println(managerName(2))
  println(managerName(1))
  println(managerName(3))
  println(managerName(7))
Some(Grace)
None
None
None

Each step may produce None; the comprehension stops at the first None. Written with nested if checks, this would be much longer.

Values and filters inside

@main def extras(): Unit =
  val result = for
    n <- 1 to 20
    square = n * n
    if square % 3 == 0
  yield s"$n^2=$square"
  println(result.mkString(", "))
3^2=9, 6^2=36, 9^2=81, 12^2=144, 15^2=225, 18^2=324

name = expression defines an intermediate value; if filters.

The same syntax works with Either, Try, and Future (upcoming lessons), and with effect types like Cats Effect's IO — one reason for-comprehensions are everywhere in Scala code.