Writing Async App in Scala
Part 1. Coding
In this part we will cover the most basic part of async programming in Scala with Futures.
Part 1. Coding.
Part 2. Exception Handling.
Part 3. Threading Model.
Today asynchronous programming gets some traction. Some may argue even some hype. However, there are cases when it’s really necessary (or, let’s put it mildly, favorably). In this blog post I’d like to put aside reasons behind writing an application in an async fashion, it’s a separate topic. Here I want to focus on the practical side of this topic — how it looks like, what we have to do in order to make it simpler, and what problems we may encounter. Everything here is based on my personal experience, so I hope it won’t be too academic.
Another thing, I’m not going to explore different approaches of achieving asynchronicity (actor model, functional approaches, etc.) I’ve chosen approach with Future model of Scala because of the least additional cost of learning. This concept is easier understandable both for me and for people around me: leap from sync jala (java style on Scala) programming to Futures is just shorter.
This post is written under the assumption that the reader is familiar with the concept of Future/Promise model and familiar with its implementation in Scala. If not, I would advise to read these articles first: Asynchronous Programming and Scala by Alexandru Nedelcu and The Neophyte’s Guide to Scala Part 8: Welcome to the Future
by Daniel Westheide (entire Neophyte’s Guide is awesome!).
After such a long disclaimer, I have one more thing to say. A kind of application that I’m having in mind is simple: a backend for DB (not really important which one) with some REST/RPC exposure to the world (or internal world), which may also communicate via the network with other applications. Regular “web” application, without any CPU heavy operations (like image processing or blockchain computations.)
Regular web application
In our synchronous world, the application may look like this:
By the end of this series I hope it will be clear, how to rewrite application in asynchronous way and avoid some dangers of async approach.
Writing in async fashion
NB: For simplicity let’s not think about
implicit ec: ExecutionContext
argument for Future operations yet. I will cover this topic in next parts.
A straight-forward rewrite would look like:
In the following examples, I’ll show how to write a bit prettier…
if-statement replacement
Suppose we have old-school future-less code:
How it would look like with futures? Suppose, all methods use some kind of IO inside, so:
Pretty straight-forward, right?
Boolean operations
What if there are multiple conditions?
There aren’t much help from scala-library itself here, so let’s extend it a bit:
And now, with these extension methods we will get this:
Nice, right?
We can go further, for example, if some of conditions aren’t IO-bound, or already calculated, we can just extend it a little bit more:
And use it:
Same code, but now we can mix Future[Boolean]
and Boolean
. Well, almost… We also need to add corresponding extension methods to Future[Boolean]
for commutativity, which is a bit more complex, because we can’t overload method with arguments => Boolean
and => Future[Boolean]
, so we need to use some implicit magic (called type classes):
I hope you’ve got the idea: whatever regular boolean operation you need, just extend Future[Boolean]
and Boolean
classes to support it, and your code will look beautiful and simple.
for-comprehensions for the rescue
Another way of making code look like future-less code is usage of for-comprehensions. This topic is covered quite a lot (1, 2, 3). In short, it looks like this:
Dealing with filter
Looks very familiar. But in real life, it’s more complicated:
if
construct is supported (via withFilter
method of Future
), of course, but Future
will be resolved with NoSuchElementException
, without ability to understand what’s really happened there.
I came up with this solution (yes, another extension method):
And now we can rewrite it in this form:
Now there will be PermissionDeniedException
if user doesn’t have permissions, but still NoSuchElementException
if movie is hidden. One of solutions might be this: instead of just comprehending getMovie
, we may make it slightly more complex:
As always, we can simplify it with simple extension method:
Much better now:
Another filter-related consideration
What I really don’t like about regular filter
is this NoSuchElementException
. When we use recover
or recoverWith
right after, it means, that this exception is created only to be catched very match soon and will be replaced with something else. In a hell for p̶e̶r̶f̶e̶c̶t̶i̶o̶n̶i̶s̶t̶s people who waste their time on micro-optimizations there is a never-ending loop:
try {
throw new NoSuchElementException
} catch {
case _: NoSuchElementException =>
}
To deal with my control freak’s issue I’ve created simple singleton exception without a stack-trace:
object ControlException extends Throwable("", null, false, false) {
def unapply(e: Throwable): Boolean =
e.isInstanceOf[ControlException.type]
}
And whenever I extend Future
in a way that I need to call recover
method after, I use this ControlException
which literally has close to zero overhead. Of course, I have convenient extension methods supporting it:
Dealing with Option
Another problem with for-comprehensions is that you can’t mix different monads (like Option
, in the world of functional programming it is solved task, this piece is for those who isn’t there ;-) ). Because in real life it’s quite common to expose methods like:
def findMovie(title: String): Future[Option[Movie]]
In this case, in for-comprehensions in this expression movie <- findMovie(“Dark Waters”)
the type of movie
will be Option[Movie]
, not Movie
. Which is understandable, but we need to find out how to deal with it. One way is to expose Option-less version which resolves Future
with some Exception
. Another approach is, as you may already have guessed, to create a convenient extension method:
And usage:
Conclusion
This part is the most basic of async programming: coding in async fashion. But still, when I started to write more and more code I realized how vanilla Scala is not sufficient to do most of the work — I import my extensions in every second file where I write something with Futures.
And because Future
functionality is very limited out of the box, don’t hesitate to extend it to make the life easier and the code more readable.
All code is available on GitHub.