Start independent Futures outside of a for-comprehension

When working with independent Futures, make sure not to initialise them inside a for-comprehension.

Reason

For-comprehension will create a dependency between your Futures, turning your code synchronous behind your back.

To understand how this can happen, it’s important to realise that for-comprehensions are just syntactic sugar for nested flatMap and map calls.

Take the following code:

import scala.concurrent._
import scala.concurrent.duration._
import ExecutionContext.Implicits.global

def longRunning(): Int = {
  Thread.sleep(100)
  println("LONG")
  1
}

def immediate(): Int = {
  println("IMMEDIATE")
  2
}

def combine(): Future[Int] = for {
  i <- Future(longRunning())
  j <- Future(immediate())
} yield i + j

combine desugars to:

def desugaredCombine(): Future[Int] =
  Future(longRunning()).flatMap { i =>
    Future(immediate()).map(j => i + j)
  }

This means that the second Future (the one that yields 2) cannot be started before the first one has completed - i is in its scope, even if not used - even though these two Futures are clearly independent from each other.

To make this evident, let’s evaluate combine. However many times you run it, LONG will always be printed before IMMEDIATE, even though the former is executed after a significant delay:

Await.result(combine(), 500.millis)
// res1: Int = 3

This can be worked around by creating the two Future instances outside of the for-comprehension: Future has the controversial behaviour that it starts executing when created, not when evaluated.

def betterCombine(): Future[Int] = {
  val f1 = Future(longRunning())
  val f2 = Future(immediate())

  for {
    i <- f1
    j <- f2
  } yield i + j
}

And if we now evaluate betterCombine, the log messages should print in the expected order:

Await.result(betterCombine(), 200.millis)
// res2: Int = 3

Checked by

This rule is not checked by any linter that I know of.