Suppose I need to run two concurrent computations, wait for both of them, and then combine their results. More specifically, I need to run f1: X1 => Y1
and <
Your post seems to contain two more or less independent questions.
I will address the concrete practical problem of running two concurrent computations first. The question about Applicative
is answered in the very end.
Suppose you have two asynchronous functions:
val f1: X1 => Future[Y1]
val f2: X2 => Future[Y2]
And two values:
val x1: X1
val x2: X2
Now you can start the computations in multiple different ways. Let's take a look at some of them.
Starting computations outside of for
(parallel)
Suppose you do this:
val y1: Future[Y1] = f1(x1)
val y2: Future[Y2] = f2(x2)
Now, the computations f1
and f2
are already running. It does not matter in which order you collect the results. You could do it with a for
-comprehension:
val y: Future[(Y1,Y2)] = for(res1 <- y1; res2 <- y2) yield (res1,res2)
Using the expressions y1
and y2
in the for
-comprehension does not interfere with the order of computation of y1
and y2
, they are still being computed in parallel.
Starting computations inside of for
(sequential)
If we simply take the definitions of y1
and y2
, and plug them into the for
comprehension directly, we will still get the same result, but the order of execution will be different:
val y = for (res1 <- f1(x1); res2 <- f2(x2)) yield (res1, res2)
translates into
val y = f1(x1).flatMap{ res1 => f2(x2).map{ res2 => (res1, res2) } }
in particular, the second computation starts after the first one has terminated. This is usually not what one wants to have.
Here, a basic substitution principle is violated. If there were no side-effects, one probably could transform this version into the previous one, but in Scala, one has to take care of the order of execution explicitly.
Zipping futures (parallel)
Futures respect products. There is a method Future.zip
, which allows you to do this:
val y = f1(x1) zip f2(x2)
This would run both computations in parallel until both are done, or until one of them fails.
Demo
Here is a little script that demonstrates this behaviour (inspired by muhuk
's post):
import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import java.lang.Thread.sleep
import java.lang.System.{currentTimeMillis => millis}
var time: Long = 0
val x1 = 1
val x2 = 2
// this function just waits
val f1: Int => Future[Unit] = {
x => Future { sleep(x * 1000) }
}
// this function waits and then prints
// elapsed time
val f2: Int => Future[Unit] = {
x => Future {
sleep(x * 1000)
val elapsed = millis() - time
printf("Time: %1.3f seconds\n", elapsed / 1000.0)
}
}
/* Outside `for` */ {
time = millis()
val y1 = f1(x1)
val y2 = f2(x2)
val y = for(res1 <- y1; res2 <- y2) yield (res1,res2)
Await.result(y, Duration.Inf)
}
/* Inside `for` */ {
time = millis()
val y = for(res1 <- f1(x1); res2 <- f2(x2)) yield (res1, res2)
Await.result(y, Duration.Inf)
}
/* Zip */ {
time = millis()
val y = f1(x1) zip f2(x2)
Await.result(y, Duration.Inf)
}
Output:
Time: 2.028 seconds
Time: 3.001 seconds
Time: 2.001 seconds
Applicative
Using this definition from your other post:
trait Applicative[F[_]] {
def apply[A, B](f: F[A => B]): F[A] => F[B]
}
one could do something like this:
object FutureApplicative extends Applicative[Future] {
def apply[A, B](ff: Future[A => B]): Future[A] => Future[B] = {
fa => for ((f,a) <- ff zip fa) yield f(a)
}
}
However, I'm not sure what this has to do with your concrete problem, or with understandable and readable code. A Future
already is a monad (this is stronger than Applicative
), and there is even built-in syntax for it, so I don't see any advantages in adding some Applicative
s here.