For comprehensions

Scala · Practical · 12 min

For comprehensions make multi-step logic over effect types read like sequential code.

A for comprehension with yield is syntax sugar for calls to flatMap, map and withFilter. The compiler rewrites

for a <- fa; b <- fb(a) yield f(a, b)

into fa.flatMap(a => fb(a).map(b => f(a, b))). Any type with those methods works, which is why the same syntax handles collections, Option, Either, Try, Future and effect types like Cats Effect's IO.

With Option and Either it expresses "do these steps in order and stop at the first failure": each <- unwraps a value, and if any step yields None or Left, the whole expression short-circuits to it.

Other parts:

  • if cond guards filter (requires withFilter; Either doesn't support guards without an empty value).
  • x = expr binds an intermediate value without unwrapping.
  • With collections, multiple generators produce a cartesian product.

All generators in one comprehension must be of the same kind: you can't mix Option and List steps directly; convert first (opt.toList, opt.toRight(err)).

Without yield, a for loop desugars to foreach and returns Unit.

Example

case class User(id: Int, name: String, managerId: Option[Int])
val users = Map(1 -> User(1, "Ana", None), 2 -> User(2, "Bo", Some(1)))

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

println(managerName(2))   // Some(Ana)
println(managerName(1))   // None (Ana has no manager)

def toAge(s: String): Either[String, Int] = s.toIntOption.toRight(s"bad: $s")
val total = for
  a <- toAge("30")
  b <- toAge("x")
  sum = a + b
yield sum
println(total)            // Left(bad: x)

val pairs = for n <- List(1, 2, 3); c <- List('a', 'b') if n != 2 yield s"$n$c"
println(pairs)            // List(1a, 1b, 3a, 3b)

Remember

  • for ... yield desugars to flatMap/map/withFilter
  • Over Option/Either it short-circuits at the first None/Left
  • All generators must be the same kind of type
  • x = expr binds a plain intermediate value

Watch out for

  • Mixing an Option generator into a List comprehension (or Future with Option) and getting confusing type errors.

Check yourself

  1. What does for { a <- Some(1); b <- None: Option[Int] } yield a + b return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scala-lang.org/scala3/book/control-structures.html#for-expressions