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 f2: X2 => Y2
concurrently and then call f: (Y1, Y2) => Y
to finally get a value of Y
.
I can create future computations fut1: X1 => Future[Y1]
and fut2: X2 => Future[Y2]
and then compose them to get fut: (X1, X2) => Future[Y]
using monadic composition.
The problem is that monadic composition implies sequential wait. In our case it implies that we wait for one future first and then we will wait for another. For instance. if it takes 2 sec. to the first future to complete and just 1 sec. to the 2nd future to fail we waste 1 sec.
Thus it looks like we need an applicative composition of the futures to wait till either both complete or at least one future fails. Does it make sense ? How would you implement <*>
for futures ?
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:
And two values:
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:
Now, the computations
f1
andf2
are already running. It does not matter in which order you collect the results. You could do it with afor
-comprehension:Using the expressions
y1
andy2
in thefor
-comprehension does not interfere with the order of computation ofy1
andy2
, they are still being computed in parallel.Starting computations inside of
for
(sequential)If we simply take the definitions of
y1
andy2
, and plug them into thefor
comprehension directly, we will still get the same result, but the order of execution will be different:translates into
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: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):Output:
Applicative
Using this definition from your other post:
one could do something like this:
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 thanApplicative
), and there is even built-in syntax for it, so I don't see any advantages in adding someApplicative
s here.None of the methods in other answers does the right thing in case of a future that fails quickly plus a future that succeeds after a long time.
But such a method can be implemented manually:
ScalaZ does a similar thing internally, so both
f1 |@| f2
andList(f1, f2).sequence
fail immediately after any of the futures fails.Here is a quick test of the failing time for those methods:
And the result on my machine is:
This is unfortunately true.
Running this code outputs:
I am not sure applicative composition has anything to do with the concurrent strategy. Using
for
comprehensions, you get a result if all futures complete or a failure if any of them fails. So it's semantically the same.Why Are They Running Sequentially
I think the reason why futures are run sequentially is because
step1
is available withinstep2
(and in the rest of the computation). Essentially we can convert thefor
block as:So the result of previous computations are available to the rest of the steps. It differs from
<?>
&<*>
in this respect.How To Run Futures In Parallel
@andrey-tyukin's code runs futures in parallel:
Output:
It needs not be sequential. The future computation may start the moment the future is created. Of course, if the future is created by the flatMap argument (and it will necessary be so if it needs the result of the first computation), then it will be sequential. But in code such as
you get concurrent execution.
So the implementation of Applicative implied by Monad is ok.