For-comprehension, a Scala language concept

Last updated

The for-comprehension is highly important syntatic enhancement in functional programming languages.

assert(List(1, 2, 3).map(num => num + 1) == List(2, 3, 4))

Becomes:

assert((for (num <- List(1, 2, 3)) yield num + 1) == List(2, 3, 4))

And

val Multiplier = 10

assert(
  List(1, 2, 3)
    .flatMap(num =>
      List(num * Multiplier - 1, num * Multiplier, num * Multiplier + 1)
    )
    .map(num => num + 1) == List(10, 11, 12, 20, 21, 22, 30, 31, 32)
)

Becomes:

val Multiplier = 10

val result: List[Int] = for {
  num <- List(1, 2, 3)
  anotherNum <-
    List(num * Multiplier - 1, num * Multiplier, num * Multiplier + 1)
} yield anotherNum + 1

assert(result == List(10, 11, 12, 20, 21, 22, 30, 31, 32))

Even more interestingly, an 'if' guard may be available for some container types, such as:

val Multiplier = 10

object Odd {
  def unapply(num: Int): Option[Int] = Option.when(num % 2 == 1)(num)
}

val result: List[Int] = for {
  Odd(num) <- List(1, 2, 3) if num > 2
  anotherNum <-
    List(num * Multiplier - 1, num * Multiplier, num * Multiplier + 1)
} yield anotherNum + 1

assert(result == List(30, 31, 32))

Interestingly, this applies to pattern matching as well, which you can do inside for-comprehensions!

Background

Prior to Scala and F#, for-comprehensions were widely used in Haskell and XQuery. The key selling point is how much more concise they can make the code. There are numerous definitions of what they do (such as being 'a way to compose Monads'), However the most-straightforward way to see them is as something that lets you combine sequences of .map and .flatMap, as well as .collect/.withFilter in a very straight-forward way.

This can include the IO type of cats-effect, as well as simple types like Option Type and other container types like Lazy List or View.

What is cool in Scala is that often Option types can be combined with Iterables, allowing for very terse syntax.

for-comprehensions are in particular useful where code becomes unreadable due to a large number of chains of .map and .flatMap - and you just want to have everything in one column. This anti-pattern of unreadable chains of code is also known as callback hell (example).

Various names are given for for-comprehensions in different languages: do-notation in Haskell, await-async in JavaScript, list comprehensions in F#, generators in Python. Even SQL queries can be considered a form of for-comprehensions.