
Good evening, gentlemen readers. Today I would like to shed some light on such a wonderful part of the
scala core called
Future . Actually there is
documentation on the official website, but there is an explanation of how to work with it using
event driven approach. But with this,
Future is also a monad. And in this article I would like to give examples and explain a little how they should be used in this way (or rather, my own vision of this issue). All interested persons will get acquainted with a question I ask under kat.
What will not be here
In this article, I'm not going to talk about callbacks, promises, how this great competition model is achieved or something else like that.
What will happen
And I will write just about the functions that give the
Future the behavior of the monad.
')
map
So let's
go to
api :
def map[S](f: (T) ⇒ S)(implicit executor: ExecutionContext): Future[S]
What does the
map do? If the
Future executes successfully, the function
f will be executed with the result passed to it. And this result will be again packed in
Future That is:
Future(5) map (2*) map println
In the end, this code will display 10 on the screen as expected. If the result is
Failure , the function will not be executed:
Future(5) map (_ / 0) map println
The second function will throw an exception and nothing will be displayed.
So why use a
map ? - for successive operations that require the result of the previous one.
flatmap
The sophisticated reader understands that a monad can be inside a monad and it is necessary to correct this matter as:
def flatMap[S](f: (T) ⇒ Future[S])(implicit executor: ExecutionContext): Future[S]
flatMap takes a function that returns another
Future , and returns it.
def f(a: Int): Future[Int] = Future(a * 5) Future(2) flatMap (f) map println
The number 10 is displayed. In case of unsuccessful execution, the behavior is the same as in
map .
flatMap should be used in the case of chain operations that return
Future .
for
No need to fly and write angrily that
for is not a function, but a syntactic construction. Yes, I know, but not to tell about her would be stupid
Kohl, since we looked at
map and
flatMap , we need to consider using with
for-comprehensions .
Let's look at some bad and good examples of using
for with the
Future :
The result of both operations will be the same, but ...
Why bad?Well, actually the answer is simple:
for { a <- longComputations1() b <- longComputations2() c <- longComputations3() } yield a*b*c
will unfold in:
longComputations1().flatMap { a => longComputations2().flatMap { b => longComputations3().flatMap { c => a*b*c } } }
That is, the whole case will be called in series, not in parallel. The second option solves this problem, but not in the best way, so only the best.
zip
def zip[U](that: Future[U]): Future[(T, U)]
Previously, the problem of using multiple
Future at the same time was considered. Well,
zip solves this problem. He takes two
Future and packs their results in
Tuple2 . And here is our example from above how to write now:
longComputations1() zip longComputations2() zip longComputations3() map { case ((a, b), c) => a * b * c }
Personally, in my opinion, everything is much cleaner and simpler.
filter and withFilter
def filter(p: (T) ⇒ Boolean)(implicit executor: ExecutionContext): Future[T] final def withFilter(p: (T) ⇒ Boolean)(implicit executor: ExecutionContext): Future[T]
Everything is logical here, take the result, test it, and if it doesn’t fit then it will be
Future with
Failed , in which
NoSuchElementException will be packed.
recover and recoverWith
If our code is executed asynchronously, we need asynchronous control over exceptions. And here it is:
def recover[U >: T](pf: PartialFunction[Throwable, U])(implicit executor: ExecutionContext): Future[U] def recoverWith[U >: T](pf: PartialFunction[Throwable, Future[U]])(implicit executor: ExecutionContext): Future[U]
In the case of an exceptional situation, a partial function will be called, which in one case should return a value, in another
Future .
Future(5) map (_ / 0) recover { case _ => 0 } map println
Here we will have an exception processed and 0 will be displayed.
foreach
In essence, it is a
map , only the result is not packaged in a new
Future , but simply returns Unit:
def foreach[U](f: (T) ⇒ U)(implicit executor: ExecutionContext): Unit
In fact, the previous examples are not entirely correct and it would be better to write:
Future(5) map (2*) foreach println
Here we avoided creating one extra Future.
A more general example
So we have:
def f1: Future[Double] def f2: Future[Double] def f3(a: Double): Future[Double] def f4: Future[Double] def f5(a: Double, b: Double, c: Double): Future[Double]
We know that the result of executing
f2 must be transferred to
f3 , and the results of executing
f1 ,
f3 and
f4 must be transmitted to
f5 . And at the end the result should be output to the standard output stream. It is also known that f3 may throw an exception, in which case
0 must be returned.
Go:
val r1 = f1() val r2 = f2() flatMap (f3) recover { case _: Exception => 0 } var r3 = f4() for { a <- r1 b <- r2 c <- r3 res <- f5(a, b, c) } yield println(res)
I prefer:
f1 zip f4 zip (f2() flatMap (f3) recover { case _: Exception => 0 }) flatMap { case ((a, b), c) => f5(a, b, c) } foreach println
How would you record?
Afterword
So, I tried to describe the monad functions of the
Future , and I personally think that I was able to. Of course, there is also something to describe, for example, a helper class, in which there are very interesting and important functions, but this is already material from another article.
Please write in the comments how you use the
Future and what I have omitted.
Thanks for attention.