The concept of Future is an encapsulation of a computational code in a way that is convenient for functional composition. Future is an abstraction that eliminates direct work with threads. The focus of development is moving from thread synchronization techniques (monitors, locks etc) to terms of chaining data transformation, callback behaviors, composability of asynchronous results. Engineers primarily focus on business logic rather than on implementational details of the logic execution. It’s a shift from imperative to declarative development, that hides details of composition and synchronization of asynchronous results of the computation.
Async vs Sync Code
In mainstream software development, synchronous technologies were dominating for decades. The slowdown in the increasing performance of CPU cores created a high level of demand in software that spreads to multi-core units. Concurrent computation models like an actor model are alive again. This day's demand in Erlang and related technology Elixir is increased. The actor model with a combination of functional software development creates a competitive advantage in delivering code faster, having a better level of maintainability of software systems.
Execution of code asynchronously provides an ability to scale out an application to multiple CPU cores. Techniques like I/O multiplexing on OS level lead to an improved performance, better threads utilization, performing more work by the same amount of threads, eliminating blocking threads in waiting state.
In Scala, a way of execution of Future is encapsulated in ExecutionContext abstraction. ExecutionContext is based on Java’s Executor abstraction and implementations might have different strategies how to execute code (even synchronously in one thread). According to requirements, there are various strategies of using a certain kind of ExecutionContext. In case of blocking I/O the threads related to ExecutionContext will be blocked and it’s recommended to increase thread pool size. There is no requirement for big thread pool to be used for non-blocking I/O. It is recommended to use different ExecutionContext for separation of different execution aspects in an application. For example, it’s necessary to separate web framework (e.g. Play), Akka, database logic, UI logic, business logic with long-running execution (e.g. heavy algorithmic computations), and short business logic execution. As a result, those parts might be scaled and monitored individually.
Synchronous Code in Unit Tests
Unit testing is a way to prove a correctness of business logic on test’s execution time. Strong type systems might give an ability to prove the correctness of certain aspects of business logic itself, and eliminate testing of contract correctness (method signatures, correct data types etc). The main requirement for unit tests is to be lightweight. Unit tests should be run regularly. High code coverage by unit tests gives a good level of maintainability. Good balance in covering related aspects (but not by lines, in FP coverage by lines is not representative) is required for high development speed. Covering together unrelated logic gives a difficulty in code maintenance because any change of touched logic required to rewrite a related test code. The more lightweight tests are, the more maintainable an application will become. It is recommended to test a specific code logic and abstracting other dependencies using the Mock technique. This gives loose coupling in tests and as the result less effort in support.
Moreover, unit tests often check lightweight logic and details of execution are not important. According to testing perspective, it is expected that the unit test executes sequentially in a synchronous way. In order to test asynchronous code, an adoption of testing code to synchronous model is required, due to waiting for a completion of asynchronous calls and verifying results as soon as they are available. Execution details might be hidden in lightweight unit tests, as the unit testing provides a verification of results, without focusing on execution implementation.
Integration tests in contrast to unit tests require integration between an internal application logic and an external code or services that are not under control. This logic is highly related to execution details (might be based on Futures) and more likely can’t be abstracted from those details.
A business logic in unit tests is highly possible to be abstracted from execution details.
Monad as Computational Unit
A Monad in Functional programming is one of base computational units, well-known design pattern in functional programming. Monad gives a possibility to chain transformation logic, compose Monads and describe business logic in monadic transformations.
Monad type class can be considered as a container or a context with a value inside. Monad must have at least two operations. The Identity operation creates Monad from a value. fmap operation applies a function to value inside Monad and as a result, creates another Monad. The exact type of Monad might be changed (it depends on applied function).
def id[T](a:T):M[T]` def fmap[A,B](m:M[A])(f:A=>M[B]):M[B]
Assume that the function is
scala f:A=>B, and value is
Left Identity Law
A value that is put into the identity context and then applied to function is equal to the same as function application on the value.
fmap(id(x))(f) == f(x)
Right Identity Law
Application of identity function to a monadic value is equal to the same monadic value.
fmap(m)(id) == m
Applications of two functions to monadic value are equal to application of first function inside the monad and after application of the second function.
fmap(fmap(m)(f1))(f2) == fmap(m)(x => fmap(f1(x))(f2))
Future is Not Exactly Monad
Future behaves as a Monad, but is a Future a Monad? Let’s check Future according to Monad Laws. Firstly, assume that Monad computes a value but doesn't make side-effects.
def id[T](value:T):Future[T]= Future.successful(value) val value: Int = 123 def f0(x: Int): Future[Int] = Future(x + 100) // Left identity Law Await.result(id(value).flatMap(f0), timeout) == Await.result(f0(value), timeout) val monad = Future.successful(value) // Right identity Law Await.result(monad.flatMap(id), timeout) == Await.result(monad, timeout) // plus 10 def f1(x: Int) = Future.successful(x + 10) // mult 30 def f2(x: Int) = Future.successful(x * 30) // Associativity Law Await.result(monad.flatMap(f1).flatMap(f2), timeout) == Await.result(monad.flatMap(x => f1(x).flatMap(f2)), timeout)
Breaking Monad Laws
A potential side effect might break monad laws. A side effect itself is an important aspect of application behavior because any execution begins from an effect (e.g. I/O). As far as Future works with values it behaves as a Monad. But it is no more Monad in case of encapsulating of side effect.
Monadic Abstraction as Future Replacement
fmap operation gives an ability to have monadic transformations on type classes.
Business logic and tests might be implemented in terms of monadic transformations.
A deep nested monadic composition is often required for business logic. Monad transformers help to solve computations on complex Monad compositions. Monad transformer is a way to work with nested monads like with a simple (one layer) monad.
An example of complex monad compositions:
Future[Either[String,Seq[String]]] From the developer’s perspective, it would be useful to work directly with
Seq[String] in Right part of Either. The solution without using Monad transformers requires a lot of boilerplate code fragments. Cats library provides useful EitherT Monad Transformer for composition Either and other monads like Option or Future
But itself Monad transformers aren't aware of the implementation of the top-level monad in composition. Future, as monadic behavior abstraction, fits Monad transformer composition well. Consequently, business logic is described in terms of monadic transformations, using both Monad and monadic abstractions like Future. In addition, Futures are not used in unit tests code. Identity monad (Id) is an empty implementation of Monad. It’s a good replacement of Future in unit tests.
Abstracting from Future leads to decreasing the execution time of unit tests, but it is not the main advantage. Decoupling execution details from business logic. Loose coupling in code results to flexibility in extending of the codebase. Pure function advantages. This solution encourages the usage of pure functions and design application in advanced functional style. Elimination of side effects makes an application more testable and maintainable. Of course, this solution makes application code more abstract and general. It might create a certain barrier for newcomers. And the application design should be chosen primarily focusing on technical requirements and real practical advantages.