Scala/Play, Part 5: From flatMap to For-Comprehensions
For-comprehensions are the first piece of Scala syntax that made me stop and say: this is neat. After learning Option, map and flatMap, I could finally read a sequence of dependent lookups without losing sight of what each method was doing. That small moment of understanding is one of the things I love about this job.
In this Scala/Play lesson, I find a policy, find its contact email, and build a label. Both lookups may find nothing. The examples use Scala 2.13.18; our later framework work targets Play 3 and Apache Pekko. There is no HTTP call or database in this exercise.

Why the second lookup needs flatMap
Our existing OptionLesson.findPolicy returns Option[PolicySnapshot]. The next method, in FlatMapLesson, returns an optional email:
def findContactEmail(policy: PolicySnapshot): Option[String] = {
if (policy.number == "POL-001") Some("customer@example.com")
else None
}
These are fictional fixtures. The known policy has an email; another policy may exist without one. If I use map with this method, its Option result becomes the value inside another Option:
val result: Option[PolicySnapshot] =
OptionLesson.findPolicy("POL-001")
val nested: Option[Option[String]] =
result.map((policy: PolicySnapshot) =>
FlatMapLesson.findContactEmail(policy))
For the successful lookup, that produces Some(Some("customer@example.com")). map has done exactly what it promises: wrap the function's result. flatMap instead lets me return the second lookup's optional result directly:
val email: Option[String] =
result.flatMap((policy: PolicySnapshot) =>
FlatMapLesson.findContactEmail(policy))
Now the result is Some(email) or None. With this representation, None does not explain whether the policy or its email was missing. That is a limit of the result type I have chosen, not something different syntax will fix.
Build a label using methods we already know
I want a label containing both the policy number and the email. The final formatting operation returns a String, so it belongs inside map:
def contactLabelWithMethods(
result: Option[PolicySnapshot]
): Option[String] = {
result.flatMap((policy: PolicySnapshot) => {
FlatMapLesson.findContactEmail(policy).map((email: String) => {
policy.number + " -> " + email
})
})
}
The inner map produces an Option[String]. The outer flatMap returns that optional result without adding another layer. Because the email lambda is inside the policy lambda, it can use the policy as well as the email.
Write the same steps as a for-comprehension
def contactLabelWithFor(
result: Option[PolicySnapshot]
): Option[String] = {
for {
policy <- result
email <- FlatMapLesson.findContactEmail(policy)
} yield policy.number + " -> " + email
}
The line policy <- result is a generator. For this Option, it means that when a policy is present, the remaining expression can use its contained value under the name policy. That name has type PolicySnapshot. It is not an Option and does not need to be declared beforehand.
The next generator introduces email as a String. Its lookup can use policy because that value was introduced by the preceding step. Both names are available to the yield expression; neither becomes a variable outside the comprehension.
yield supplies the successful result expression. Here it builds a plain String. The entire comprehension still returns Option[String], because the operations surrounding that expression are Option operations.

What the compiler translation explains
For these two simple name bindings, the comprehension translates into the outer flatMap and inner map shown above. The Scala 2.13 language specification defines this translation. I am deliberately leaving guards and more complex patterns for another lesson.
There is no unsafe get hidden behind the arrow. When the first Option is None, its flatMap does not call the lambda, so the email lookup never runs. When the email lookup returns None, its map does not call the formatting lambda, so yield is skipped. With two present values, the label is built once. The comprehension adds no asynchronous behavior.

Run and compare all three outcomes
The committed ForComprehensionLesson.scala contains both implementations. It checks each against an expected result for three scenarios: both values found, an existing policy without email, and a missing policy. The no-email case uses a directly constructed POL-002 fixture; our original findPolicy method still recognises only POL-001.
From the root of the learning repository, run:
sbt "runMain learning.PremiumLesson"
All six result assertions passed. The printed comparison is:
Methods, both found: Some(POL-001 -> customer@example.com)
For, both found: Some(POL-001 -> customer@example.com)
For, no email: None
For, no policy: None
I like the comprehension because it puts the dependency between the steps where I can see it: first a policy, then its email, then a label. And when I need to understand its behavior, I can translate it back into methods I already know. Next, I want to take that familiarity with map into collections of policies.