The worst thing in our Scala code: Futures

Author’s note: this isn’t an argument against Futures, there’s nothing wrong with them as such!  This article is about good abstraction and proper separation of concerns when using them.

The aims of functional programming are much the same as any other discipline of software engineering: modularity, abstraction, low coupling, high cohesion, and so on. While the techniques and terminology are often new to developers, the goals and benefits are expressible in very familiar language because it’s all software, and we’re trying to achieve the same thing! (Read SICP for more background on software engineering fundamentals expressed with functional ideas).

In our Scala code, using a functional style gives us a lot of software engineering advantages. Because there is a cultural and technical emphasis on immutability and isolating side-effects, we instantly gain an advantage in separating concerns and local reasoning. Great!

On the other hand, the code problems tend to be the same kind of things that plague every codebase, whether Scala, Java or Ruby. Poor choice of module boundaries; things living together that change for different reasons. Missing abstraction layers; code knowing about all kinds of things that it doesn’t need to know, and so on.

There is one thing though, that is uniquely bad in our Scala codebases—a problem that is usually missing from our Java and Ruby code—our use of futures.

Reactive!

Scala’s futures are quite nice to use in comparison to similar devices in many other languages: we can put code in a Future, where it will run asynchronously on who-knows-what-thread; using flatMap and map, we can continue to manipulate the results of the Future, independently of whether any have completed or failed.

In general-purpose applications, this technique is often used to avoid synchronous I/O. Whenever we query a database, call another service or load a file from disk, the current thread is blocked, doing nothing while the external process completes. By running the external processes asynchronously, server threads can be reallocated and used more sparingly, reducing load and enabling greater scaling. This has been widely promoted in Scala (and in other communities) under the loose banner of “Reactive” programming.

YAGNI

Programmers should have imbibed this principle with their mothers’ milk: don’t build things that you don’t need yet, and may never need at all. You Ain’t Gonna Need It. In practice, this mistake gets constantly made by engineers at all levels, often distracted and seduced by the challenge of a difficult problem, or a trendy solution.

Futures, and “reactive” techniques in general are an excellent solution to certain kinds of problems, but do we have those problems? Not every codebase will need to scale endlessly, and even then thread-pooling might not be the bottleneck. The great majority of our traffic goes through a small number of systems, and the great majority of our systems have a small number of users. Futures can be a valid and effective optimisation technique, but should be used for a concrete app-specific reason, and not just because they are there, and cool.

Separation of concerns

When we have used Futures in our Scala codebases, so far it has always been to “add asynchronous I/O” to code that would otherwise make perfect sense without it. This suggests that it is a totally separate concern to the other things in the code; they should be able to exist without knowing or caring about asynchrony, and this feature should be addable and removable without changing much code.

In practice however, adding or removing Futures usually amounts to a rewrite.

  • They sit on every type signature, and code must flatMap over them to manipulate their values.
  • Plain values must be lifted into an already-completed Future context.
  • At the end, callbacks must be supplied to handle the eventual success or failure of the result.
  • Testing is really hard and clumsy. Timeouts ahoy!

In addition, since we are using the standard library implementation scala.concurrent.Future:

  • scala.concurrent.Future is built to work naturally with scala.util.Try, so our code often ends up clumsily using Try everywhere to represent failure, using raw exceptions as failure values even where no exceptions get thrown.
  • To do anything with a scala.concurrent.Future, you need to lug around an implicit ExecutionContext. This obnoxious dependency needs to be threaded through everywhere they are used.

There must be a better way to separate our concerns!

Abstraction is the answer

Abstraction is one of the most important ideas in computing; at heart it is about hiding information. It is, in this sense, always a good thing. We shouldn’t know about or depend on things we don’t need; this results in worse software in multiple ways. It becomes:

  • Less reusable, because the extra information places unnecessary constraints on usage
  • Harder to test, because the extra information requires specification and provision
  • Harder to understand, because there are irrelevant details obscuring the essence of the code.
  • Harder to maintain, because future modifications to the software will inevitably come to depend on the extra information “conveniently” at hand. Ease of refactoring will degrade sharply at this point.

For instance, a function is an abstraction, because it hides information about the specific argument values; the function must operate on all possible argument values. Type parameters (“generics”) are an abstraction, because we are hiding information about the specific type; the functionality must be defined for all possible types in that position.

Case study

So what is the minimum information that we require for our average Future-toting Scala program? Let’s look at a concrete example, adapted from some production code:

final def load(pageUrl: URL)(implicit ec: ExecutionContext): Future[Page] =   
  for {  
    page <- consumePage(pageUrl)  
    actionedPage <- agencyWriter.writeAgencies(page)  
    _ <- elasticSearchService.updateLatestChange(actionedPage.toString)  
  } yield actionedPage

The code is expressed with Scala’s for/yield syntax; consumePage, writeAgencies and updateLatestChange all return Futures. Each line containing a <- performs a flatMap on the Future value to its right, allowing us to write essentially imperative code that is oblivious to the fact that the actual results don’t exist yet. We are forced to supply an implicit ExecutionContext so that the Futures can do their work.

Again, we can see that this code has nothing to do with futures; the only information that we require is that we need to flatMap each result, and then lift the resulting actionedPage back into the context. In other words, all we need to know is that we are using a monad:

trait Monad[F[_]] {
  def flatMap[A,B](fa: F[A], f: A => F[B]): F[B]
  def pure[A](a: A): F[A]
}

We don’t need to know about ExecutionContexts, we don’t need to know about callbacks or Trys, we just need an implementation of this interface for any old F[_]. Assuming that we rewrite consumePage and the others in the same way, now we can make the code more abstract:

final def load[F[_]](pageUrl: URL)(implicit m: Monad[F]): F[Page] =  
  for {  
    page <- consumePage(pageUrl)  
    actionedPage <- agencyWriter.writeAgencies(page)  
    _ <- elasticsearchService.updateLatestChange(actionedPage.toString)  
  } yield actionedPage

Now our return type is F[Page], for all F, where there’s an implicit instance of Monad[F]. This usage of implicits is called a typeclass; when we call the method, our choice of a concrete F[_] will allow the compiler to infer the required behaviour. Instead of inferring types from the code, we are inferring code from the types! We can use some syntax sugar to rewrite our method signature:

final def load[F[_]: Monad](pageUrl: URL): F[Page]

We can read the type parameter as “for all F, constrained by Monad”.

So now we have shed some ballast and produced, simpler, tighter, more abstract code. What have we bought?

Testing

object Test {
  "loading such-and-such a URL" should {
    "produce such-such a page" in {
       val someUrl = "http://someurl"
       val result: Page = load[Id](someUrl)
       result should beEqualTo(expectedPage)
    }
  }
}

For the time being, let’s assume we’re swapping out the I/O somehow, so this is a pure test. See how we provided the type Id instead of Future? An Id[Foo] is just a Foo. The Futures have completely melted away, and our code didn’t change. Input, output, assert.

// In scalaz
type Id[A] = A
implicit def idMonad: Monad[Id] = ...

Production

For the real thing, of course we can keep our Futures intact, and provide the Future-specific handling code at the end.

// Assuming a monad definition for Future
implicit def futureMonad(implicit ec: ExecutionContext): Monad[Future] = ...

object Application {
  def doStuff(config: Config): Future[Page] = {
    load[Future](config.theRealUrl)
  }
}

The load functionality can run synchronously, asynchronously, and with or without dependencies, logging, mutable state, or any other concern representable in a monad.

Other approaches

In some of our codebases, we use the “Interpreter pattern”, where we use free monads to build our program as a kind of script, composed out of declarative actions. Later, an interpreter module interprets the actions, performing effects for real code, or fixed pure results for tests. This separation of concerns allows us to run effects asynchronously in the interpreter, without bogging down the code with inappropriate knowledge of the execution strategy. In this case too, we have set an abstraction boundary, and put code on one side, and asynchrony on the other.

The drawbacks specific to scala.concurrent.Future can of course be avoided by using a different implementation, such as Scalaz’s Task, which separates the declaration of the Task from its execution.

Use only what you need, know only what you need

So how should we proceed? While we’ve covered some interesting FP tools here, in fact all we want is to satisfy basic software engineering principles.

  • If you don’t need asynchrony given the SLAs and expected usage of your application, then don’t use it. The code will be simpler.
  • If you do, then restrict knowledge of it to parts of the code that genuinely need to know about it. Aggressively keep Futures away from your logic, so that everything of importance can be understood and tested independently.
  • Learn and use the abstraction tools available to us; in Scala, higher-kinded types, implicit typeclasses, and monads give us a clean and powerful way to separate our declarative intent from our messy execution policies.

Further reading