Skip to content

Instantly share code, notes, and snippets.

@nrinaudo
Created January 6, 2019 11:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nrinaudo/3e963ecfdcac2881cf5f9756adbc13df to your computer and use it in GitHub Desktop.
Save nrinaudo/3e963ecfdcac2881cf5f9756adbc13df to your computer and use it in GitHub Desktop.
title layout
Start independent Futures outside of a for-comprehension
article

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)

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)
@nrinaudo
Copy link
Author

nrinaudo commented Jan 6, 2019

This fails under mdoc 1.2.6 with the following:

java.lang.NoClassDefFoundError: Could not initialize class repl.Session$App$
	at repl.Session$App$$anonfun$combine$1.apply$mcI$sp(future_in_comprehensions.md:31)
	at repl.Session$App$$anonfun$combine$1.apply(future_in_comprehensions.md:31)
	at repl.Session$App$$anonfun$combine$1.apply(future_in_comprehensions.md:31)
	at scala.concurrent.Future$.$anonfun$apply$1(Future.scala:658)
	at scala.util.Success.$anonfun$map$1(Try.scala:255)
	at scala.util.Success.map(Try.scala:213)
	at scala.concurrent.Future.$anonfun$map$1(Future.scala:292)
	at scala.concurrent.impl.Promise.liftedTree1$1(Promise.scala:33)
	at scala.concurrent.impl.Promise.$anonfun$transform$1(Promise.scala:33)
	at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:64)
	at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
error: /Users/nicolasrinaudo/dev/nrinaudo/scala-best-practices/src/main/mdoc/_tricky_behaviours/future_in_comprehensions.md:52:1: Futures timed out after [500 milliseconds]
Await.result(combine(), 500.millis)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment