Can't get my head around Future(s)

I had a look at the documentation of Futures but I can’t get my head around it.

As far as I understand

val f = Future {
  /* do some long-running task */
}

If I want to execute any code after the task has been completed, I can use a call-back like so:

f onComplete { ... }

But since onComplete returns Unit, I don’t understand how I can get hold on the result of whatever I want to execute after the Future has completed.

I have the feeling, that I did not understand the concept of Futures, maybe someone could enlighten me!

A callback is something that receives a value, like

f onComplete {
case Success(value) => myCallbackForTheValue(value)
case Failure(exception) => myCallbackForTheException(exception)
}

The whole point of a callback is waiting until the value is available.

1 Like

I see. So in onComplete I call the callback. On what thread / EC will the call of the callback executed?

onComplete takes an implicit ExecutionContext, so that’s where the callback will run.

1 Like

Using onComplete, you have to handle the result with a side effect. To use the result of a future in a more functional way, you usually transform it with map and flatMap calls (or using for-comprehensions), which take a function that transforms the (successful) result of the future and return a new future with the result. There are also functions like zip, which let you combine Futures, possibly running them in parallel depending on the given execution context.

You do this until you come to the point, where you want to handle possible failures and get out of the Future context. For this, you can use Await.ready(f, duration) (documentation), which returns the Future, as soon as it is completed (but blocking the calling thread). Then you can call value on the Future, giving you Some(x: Try[T]).

All this is a bit messy in my opinion, the various functional IO libraries provide cleaner solutions (e.g. cats-effect, zio), but if you aren’t using functional libraries yet, they may be a bit much. If you stay with Futures, I’d recommend keeping the Await part at the edge of the program and staying in Future as long as possible (some frameworks encourage this approach, e.g. Play has separate routing entry points that expect you to return Futures, so you’ll never need to use Await directly. Similarly using effect libraries, cats-effect and zio have IOApp, which replace your main with a function returning IO[Unit]).

4 Likes

Thank you for your explanations!

For now, I have the feeling that I am not enough experienced in Scala to make the next step towards cats or zio, first I want to understand how things work in vanilla Scala.

One addition to the explanation from @crater2150 – in some environments (eg, the Play Framework web server), it’s entirely normal to just stay in Future the whole way. You just keep transforming your results using map and flatMap, and return a Future to the web server – it is smart enough to know what to do with it. In something like that, you basically never use Await. In my dayjob (all Scala), I essentially never use it outside of test code, even though almost all of my code uses Futures.

Really, the heart of using Futures is that you should avoid asking the question, “What is in this Future?” (Which is basically where you need Await.) Instead, focus on “Once there is a value in this Future, what should I do with it?” – that’s what map and flatMap are for, and you just use those to build chains of what-to-do-next.

4 Likes

I would like to take this opportunity to describe my use-case; perhaps Future are not even appropriate for this.

Given a desktop application with JavaFX UI. At a certain point, the user triggers a long-ish running task. To indicate that the task is still running a dialog with a progress indicator is shown. After the task has finished, the dialog with the progress indicator should be either closed (if the task finished successfully) or an error message should be displayed.

The return type of the task is Either[Error, Unit] because it has only side-effects but no actual result value.

  1. The dialog is opened on the UI thread.
  2. The long-running task is executed on another thread to not block the UI thread.
  3. After the task has finished, either the dialog is closed or an error message is displayed.

I want to use a Future to execute the task on another thread than the UI thread.

While you do not need Futures for that since plain old threads may just work, I personally would still use Futures. Why? Because it would be simpler and it can allow you to mix in more Futures in the future (pun not intended)

What I would do is make the method that runs the long-running task to return a Future[Unit] from the beginning, instead of Either[Error, Unit] (as long as Error extends Throwable). and it would probably need an implicit ExecutionContext; Another common design, would be that the class that has the long-running method receives the ExecutionContext in its constructor, that way callers of it do not need it, but given that then if users need to assign callbacks to that Future and for that they need the ExecutionContext this approach is not so common with Futures (it is with effect systems like cats-effect, Monix or ZIO).
Then, you would create your custom ExecutionContext at the beginning of your app and pass it down to whoever needs it, then when the user clicks that button you would launch the Future and register some callback using onComplete; remember that Futures are eager, as such you must be sure to only create it when the button was clicked.

For closing the dialogue or displaying the error, basically what you need to do is that in the onComplete you launch some event so the GUI even listeners would react to it; but it has been ages since the last time I programmed a GUI in the JVM.

BTW, I kind of remember Java to have one GUI component specially designed to show the process of long-running tasks, you still needed to run the task on another thread manually, but it had a mechanism to update progress and display final results.

1 Like

Thank you so much for your elaborate answer!

Luckily managing the UI is not an issue for me, I work quite a lot with JavaFX and I know its means, and so I can focus on the challenges Scala has to offer :slight_smile:

My Error does not extend Throwable since I try to catch exceptions as soon as possible and transform it into my Error types. Why does this change the approach with Futures?

Futures have a builtin failure mode based on Throwable - a Future[T] is similar to an Either[Throwable, T] in that regard. If your Error is not a Throwable, you cannot propagate them transparently inside a Future, but you’ll have to use Future[Either[Error, T]]. Which is not bad at all. A common idiom is to use the error type in the Either for failures the client is expected to handle (bad input,…) while “bad things just happen” conditions (service down,…) are covered by the Future failure mode - somewhat like checked/unchecked exceptions in Java.

3 Likes

Okay, then I understood it correctly. The error path of a Future only happens if an expectation has been propagated to the ExecutionContext.

In my simple use case, do I have to care about Promises or can I leave them aside completely?

If you mean Exception then yes, Future will catch and retain any non-fatal Exception that is thrown and you can use combinators like recoverWith to handle them. In this case, onComplete gives you access to it so both approaches can work. For example, if you decide to raise your errors in the Throwable channel of Future you can recover then in the onComplete to show the error message; or you can have three outcomes in onComplete one if it is a success, one if it is an business / logic / expected error and a third one if it is an unexpected error (many people says that here what you should do is just log it and let the program crash).

I believe you can simply ignore them, Promises are a low level construct used to create Futures from asynchronous computations.

1 Like