for-Expressions Are Better Than getOrElse (Scala 3 Video)
A great thing about using for
expressions that might not be immediately obvious is that they’re better than using a series of getOrElse
calls. I’ll demonstrate what I mean in this lesson.
The for
expression “Happy Path”
Imagine asking a user to input four Int
values as strings. You could then sum those values using this for
expression:
val sum = for {
a <- makeInt(input1)
b <- makeInt(input2)
c <- makeInt(input3)
d <- makeInt(input4)
} yield a + b + c + d
Next, assume that they gave you exactly what you asked for, four strings that properly convert to integers, like this:
val sum = for {
a <- makeInt("1")
b <- makeInt("2")
c <- makeInt("3")
d <- makeInt("4")
} yield a + b + c + d
This specific example shows the Happy Path for this for
expression. When I say, “Happy Path,” it means that you got the input values you desired, and therefore the for
expression completes properly, with sum
ending up as Option(10)
.
The for
expression “Unhappy Path”
But a great thing about the for
expression is that it also handles the “Unhappy Path,” i.e., the path that is followed when one or more of the String
values don’t convert to Int
values:
val sum = for {
a <- makeInt("ka-boom!")
b <- makeInt("2")
c <- makeInt("3")
d <- makeInt("4")
} yield a + b + c + d
Can you guess what happens with this code?
Does it throw an exception? No, of course not, pure functions don’t throw exceptions!
Does sum
end up with a value? Yes, it does. sum
is bound to None
, as the REPL shows:
scala> val sum = for {
| a <- makeInt("ka-boom!")
| b <- makeInt("2")
| c <- makeInt("3")
| d <- makeInt("4")
| } yield a + b + c + d
sum: Option[Int] = None
What happens is that the for
expression short-circuits when this line of code is reached:
a <- makeInt("ka-boom!")
When makeInt
receives a String
it can’t transform into an Int
, these things happen:
makeInt
returns aNone
- The
for
expression short-circuits at that point sum
is bound toNone
The None
result tells you that makeInt
was given something it couldn’t convert to an Int
.
It’s important to note that even though this is an Unhappy Path, the good news is that the bad input doesn’t completely blow up your
for
expression; the expression simply yieldsNone
(which is much better than doing something like throwing an exception).
Compare that to getOrElse
Compare that code to the only thing you can do with getOrElse
:
val sum = makeInt("ka-boom!").getOrElse(0)
+ makeInt("2").getOrElse(0)
+ makeInt("3").getOrElse(0)
+ makeInt("4").getOrElse(0)
When you paste that code into the REPL, you’ll see that sum
is assigned to 9
— a completely different result!
If you think that’s bad, this example just uses Option[Int]
values; imagine what could happen when you use complex, real world classes like Option[Person]
, Option[Pizza]
, or Option[List[List[Friend]]]
.
This is one reason why experienced Scala/FP developers tell you not to use getOrElse
.
Another good thing about using for
expressions that might not be immediately obvious is that they’re better than using a series of getOrElse
calls. I’ll demonstrate what I mean in this lesson.
The for
expression “Happy Path”
Imagine asking a user to input four Int
values as strings. You could then sum those values using this for
expression:
val sum = for {
a <- makeInt(input1)
b <- makeInt(input2)
c <- makeInt(input3)
d <- makeInt(input4)
} yield a + b + c + d
Next, assume that they gave you exactly what you asked for, four strings that properly convert to integers, like this:
val sum = for {
a <- makeInt("1")
b <- makeInt("2")
c <- makeInt("3")
d <- makeInt("4")
} yield a + b + c + d
This specific example shows the Happy Path for this for
expression. When I say, “Happy Path,” it means that you got the input values you desired, and therefore the for
expression completes properly, with sum
ending up as Option(10)
.
The for
expression “Unhappy Path”
But a great thing about the for
expression is that it also handles the “Unhappy Path,” i.e., the path that is followed when one or more of the String
values don’t convert to Int
values:
val sum = for {
a <- makeInt("ka-boom!")
b <- makeInt("2")
c <- makeInt("3")
d <- makeInt("4")
} yield a + b + c + d
Can you guess what happens with this code?
Does it throw an exception? No, of course not, pure functions don’t throw exceptions!
Does sum
end up with a value? Yes, it does. sum
is bound to None
, as the REPL shows:
scala> val sum = for {
| a <- makeInt("ka-boom!")
| b <- makeInt("2")
| c <- makeInt("3")
| d <- makeInt("4")
| } yield a + b + c + d
sum: Option[Int] = None
What happens is that the for
expression short-circuits when this line of code is reached:
a <- makeInt("ka-boom!")
When makeInt
receives a String
it can’t transform into an Int
, these things happen:
makeInt
returns aNone
- The
for
expression short-circuits at that point sum
is bound toNone
The None
result tells you that makeInt
was given something it couldn’t convert to an Int
.
It’s important to note that even though this is an Unhappy Path, the good news is that the bad input doesn’t completely blow up your
for
expression; the expression simply yieldsNone
(which is much better than doing something like throwing an exception).
Compare that to getOrElse
Compare that code to the only thing you can do with getOrElse
:
val sum = makeInt("ka-boom!").getOrElse(0)
+ makeInt("2").getOrElse(0)
+ makeInt("3").getOrElse(0)
+ makeInt("4").getOrElse(0)
When you paste that code into the REPL, you’ll see that sum
is assigned to 9
— a completely different result!
If you think that’s bad, this example just uses Option[Int]
values; imagine what could happen when you use complex, real world classes like Option[Person]
, Option[Pizza]
, or Option[List[List[Friend]]]
.
This is why experienced Scala/FP developers tell you not to use getOrElse
. When you first start working with Scala it seems like getOrElse
could be a good thing, but (a) it can also be an incorrect thing when used in the wrong places, and (b) in the long run it can keep you from seeing a much better solution in the for
expression.
(Embrace the idioms!)
Summary
In the last two lessons I showed a for
expression like this:
val sum = for {
a <- makeInt(string1)
b <- makeInt(string2)
c <- makeInt(string3)
d <- makeInt(string4)
} yield a + b + c + d
I mentioned that the “Happy Path” is when all of the String
values can be converted to Int
values, and the “Unhappy Path” is what happens when a String
value can’t be converted to an Int
.
Also, even though that code can be said to have Happy and Unhappy paths, the great thing about the for
expression is that it handles both cases. In the Happy case sum
is bound to an Option[Int]
, and in the Unhappy case sum
is bound to None
.
In this lesson I also demonstrated that this code is better than a getOrElse
approach because getOrElse
can give you a wrong (or inappropriate) answer. Beyond that, as you’ll see in the rest of this book, it can also keep you from embracing the Scala idioms that will lead you to a new style of coding.
Update: All of my new videos are now on
LearnScala.dev