Julien Truffaut
6th December 2023
For-comprehensions, a staple in Scala programming, can be wielded across various scenarios, making them a powerful yet potentially daunting feature, especially for newcomers. In this exploration, we'll delve into the most prevalent use cases of for-comprehensions, focusing on their applicability.
One of the most intuitive and frequently encountered use cases for for-comprehensions is iterating over collections. The syntax is not only clean but also elegant, enhancing the readability of your code. Consider the following example:
val numbers = List(1, 2, 3, 4)
val doubledNumbers = for {
number <- numbers
} yield number * 2
// doubledNumbers: List[Int] = List(2, 4, 6, 8)
For-comprehensions extend beyond single collections; they seamlessly handle multiple collections, akin to nested loops:
case class Rectangle(width: Int, height: Int, color: String)
for {
width <- List(2, 6, 10)
height <- List(3, 5)
color <- List("Blue", "Red")
} yield Rectangle(width, height, color)
// res: List[Rectangle] = List(
// Rectangle(2 , 3, "Blue"),
// Rectangle(2 , 3, "Red" ),
// Rectangle(2 , 5, "Blue"),
// Rectangle(2 , 5, "Red" ),
// Rectangle(6 , 3, "Blue"),
// Rectangle(6 , 3, "Red" ),
// Rectangle(6 , 5, "Blue"),
// Rectangle(6 , 5, "Red" ),
// Rectangle(10, 3, "Blue"),
// Rectangle(10, 3, "Red" ),
// Rectangle(10, 5, "Blue"),
// Rectangle(10, 5, "Red" ),
// )
Scala often employs types like Option
, Try
, or Either
to manage potentially failing code. For-comprehensions shine in composing these types, enabling a focus on the happy path—when no errors occur:
case class User(username: String, dateOfBirth: LocalDate)
def validateUsername(username: String): Either[String, String] =
if(username.length < 8) Left("Username is too short (8 characters minimum)")
else Right(username.toLowerCase)
def validateDateOfBirth(dob: LocalDate, today: LocalDate): Either[String, LocalDate] =
if(dob.plusYears(18).isAfter(today))
Left("User must be 18 years old or older")
else
Right(dob)
def validateUser(username: String, dob: LocalDate): Either[String, User] =
for {
username <- validateUsername(username)
dob <- validateDateOfBirth(dob, LocalDate.now())
} yield User(username, dob)
validateUser("Bob", LocalDate.of(2000, 12, 1))
// res = Left(Username is too short (8 characters minimum))
validateUser("Bob_12345", LocalDate.of(1960, 3, 5))
// res = Right(User(bob_12345, 1960-03-05))
The validateUser
method elegantly defers error handling to the for-comprehension which will return the first error encountered if any.
Asynchronous programming often leads to callback hell due to nested indents for each asynchronous method. For-comprehensions come to the rescue by flattening asynchronous logic, transforming it into a more readable, synchronous-looking structure:
def fetchUser(userId: UserId): Future[User] = ...
def sendEmail(
emailAddress: String,
title : String,
content : String
): Future[Unit] = ...
def resetPassword(userId: userId): Future[Unit] =
for {
user <- fetchUser(userId)
_ <- sendEmail(user.emailAddress, "Reset Password", "...")
} yield ()
For-comprehension stands as a formidable tool with diverse use cases. Today, we explored its applications in collection iteration, error handling, and asynchronous programming. As you deepen your Scala expertise, you'll find advanced use cases, such as leveraging for-comprehensions for dependency injection. Embrace the elegance and power of for-comprehensions in your Scala journey.