📜 ⬆️ ⬇️

Back to the Scala Future


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 :
 // for { a <- longComputations1() b <- longComputations2() c <- longComputations3() } yield a*b*c // val f1 = longComputations1() val f2 = longComputations2() val f3 = longComputations3() for { a <- f1 b <- f2 c <- f3 } yield a*b*c 

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.

Source: https://habr.com/ru/post/233555/


All Articles