7 minutes read

Just imagine that there is a system with a user model and a few implemented methods to extract user data and get its balance:

case class User(id: Long, login: String, active: Boolean)

def findUser(id: Long): Option[User]
def getUserBalance(id: Long): Option[BigDecimal]

We want to implement the method that returns the data of an active user with its balance. So, we start from getting the data of the user, making sure that the user is active. After that, we get the balance by the user id:

def getActiveUserAndBalance(id: Long): Option[(User, BigDecimal)] =
  findUser(id)
    .withFilter(user => user.active)
    .flatMap(user =>
      getUserBalance(id)
        .map(balance => (user, balance))
    )

The implementation looks simple, but the withFilter, flatMap, map chain seems a bit overloaded. Let's take a look at a unique language construct in Scala that solves this problem!

for-comprehension

The following code is arguably much clearer than the solution using map, flatMap, and withFilter that we have developed previously:

def getActiveUserAndBalance(id: Long): Option[(User, BigDecimal)] =
  for
    user    <- findUser(id) if user.active
    balance <- getUserBalance(id)
  yield (user, balance)

As you might guess, this code does the same as the code above. It doesn't change the function invocation but looks different. Also, there are several new syntaxes such as for ... yield ... expression, an arrow <-, and an if statement. So combination of them is called for-comprehension. Let's have a look at how each of them works.

The arrow operator <- translates to flatMap, and the lines below will have access to the created variable:

user <- findUser(id) // findUser(id).flatMap { user => ... }

The if statement is referred to as filter expression or guard, and you can use as many guards as needed for a problem. They can be placed on the same line or on the next one:

user <- findUser(id)
if user.active // .withFilter(user => user.active)

An expression after yield translates to map:

yield (user, balance) // .map(_ => (user, balance))

We can also create a variable with an assignment operator, but without the val keyword. This will also be translated to map but in a more complex way.

user <- findUser(id)
uppered = user.login.toUpperCase 
// findUser(id).map { user => val uppered = user.login.toUpperCase; (uppered, user) }

It's worth noting that in Scala 2 such expressions must have curly brackets for { ... } yield .... In Scala 3, you can skip them.

Just syntactic sugar

We have seen how we can use for-comprehension on the Option data structure. But we can also use it on many other kinds of data: Either, Seq or even complicated IO constructions and async computing because this functionality works on anything that has flatMap, map, and withFilter methods.

Here are two examples that show how for-comprehensions are used.

Let's imagine we need to get all pairs of numbers from 1 to N whose sum is 10. This is how you can solve the problem with for-comprehension:

def all10Pairs(n: Int): Seq[(Int, Int)] = 
  for
    i <- 1 to n
    j <- 1 to n
    if j + i == 10
  yield (i, j)

The second example shows a workflow with Either when Left handles error messages:

def divideSafe(x: Int, y: Int): Either[String, Int] =
  if (y == 0) Left("Cannot divide by 0")
  else Right(x / y)

def divideTwiceSafe(x: Int, y: Int): Either[String, Int] =
  for
    mid <- devideSafe(x, y) // the computation will stop here if y is 0
    result <- divideSafe(mid, y)
  yield result

Although the construction may seem complicated at first glance, after some practice, it turns out to be a convenient tool for combining calculations. It allows you to use many functional combinators in an imperative style and increases the readability of the code. Moreover, if you want to be sure that a for-comprehension expression is converted correctly, you can use the function Desugar Scala Code provided by IDEA:

An example of using the function Desugar Scala Code

Conclusion

In this topic, you explored the for-comprehension construct, which is often used in Scala code. You saw that the construct is just syntactic sugar because its code translates to a sequence of method calls map, flatMap, and withFilter. You also learned in which cases it's a good idea to use this construction to make the code readable.

How did you like the theory?
Report a typo