What makes Futures so great?
If you’re coming from a lower level language, you might be accustomed to using threads and processes to perform tasks concurrently. In Scala, there exists a concurrency construct that works at a much higher level called a Future. A Future represents a value that will be available in the future, or the exception that occurred while evaluating that value. So, what makes Futures so great? A Future is a concurrency abstraction that represents a future value and comes with a very powerful and convenient API that lets you deal with that future result in a type-safe and high level manner. A Future can be in three possible states: it can either be scheduled/running, failed, or successful.
M is for Monad
Like many things in Scala, a Future is a monad (yes, the scary ‘m’ word). Some like to define monads as “a monoid in the category of endofunctors” (whilst this definition may be helpful for those of you strong in Category Theory, I’ve found this James Iry’s blog post less intimidating for understanding the basics of Monads and their motivation.
The Future monad is a container that holds the result of a concurrent computation, and information about whether it succeeded or failed.
To run a computation asynchronously, all you have to do is pass a block of code into the Future constructor:
Take note that though this was the output, any concurrent computations are nondeterministic and this is only the case due to the large disparity in the thread sleep times.
So what exactly did that code do?
future1 is a Future that started running as soon as it was defined, same with future2, then we defined some callbacks on future1 and future2 with the foreach, and then immediately printed “What’s up?”. As you can see, future1 and future2 are non-blocking calls, you can put blocking code inside of Futures and the entire block of code will not block in the context of the main program.
Now you might be thinking, “So how’s this any better than a Thread? I could do most of this in a thread without any problems.” And you would be right, if that’s all we’re doing, we almost might as well use a thread – but one great aspect of a Future is its great composability. What if you wanted to take the return values of two Futures and then combined or used them together?
With map and flatMap, that becomes easy:
Because Future is a monad, it has a map, filter, and flatMap defined for it. This is all the functionality a container type has to implement in order to be considered a monad. This monadic API allows the end user perform certain useful actions on a Future.
A Future takes an implicit ExecutionContext (which you can think of as a thread pool) and performs some work asynchronously, there are many different ones you can use all with different advantages to them, in this post, we’ll be using the global one. So you’ve got a Future containing some value, what’s the use of that? This is when all the functional combinators (e.g. map, flatMap, foreach) associated with monads comes in handy. Instead of waiting on the result of a computation (and blocking) you can define behaviour that occurs upon the completion of a Future. There are many combinators that are usable with a Future, they can let you do many things like return a Future containing a different value, or not return a value. They allow you to write very linear and clean looking code.
The functional combinators allow you to linearize the flow of your code and makes it much easier to read and refactor as opposed to registering multiple nested callbacks.
Underneath, I will implement prepareIngredients in three ways, the first two methods work exactly the same, but the third method performs all operations concurrently:
Take note that with this code, we only have a futurePie. If we want to do something once that pie is done, we can register a callback with onComplete that does something with that pie:
With onComplete, you can pattern match on the result of the Future and perform different tasks based on what happened. As you can see, if my pie was successfully made, I’ll eat it, if it failed, I will cry and throw the exception that caused the failure.
Because of the way map and flatMap are defined for Futures, if any of the above steps throw an exception, they will short circuit prepareIngredients and instead of having a Future of a Pie, you’ll have a Future[Throwable] which can be handled with recover which is another combinator for gracefully recovering from error.
There are many other combinators which you can use to write really composable and clean code. I would definitely take some time to get familiar with them, Twitter has posted a great tutorial on Scala and many basic combinators here (https://twitter.github.io/scala_school/collections.html).
Your Future with Futures
Overall, Futures are a great concurrency abstraction, they give you control comparable to Threads, but with more composability, more type-safety, and cleaner semantics. With the content in this post, you should know enough to start writing some non-blocking concurrent code. For further background in monads and Futures, I would really suggest reading Scala’s documentation here.
About the Author
Kevin Lim is a fourth year Computer Engineering student at UBC and was a Co-op student at Hootsuite on the Publishing Team during the summer. In his spare time he enjoys playing Badminton, video games, and functional programming.