Future[_] vs Future[Unit] or which return type should I choose?

Recently I have a heated debate with my colleague about a proper return type for async procedures (procedure is just a function that returns Unit in Scala.) My point of view was that Future[Unit] is the right type. Let me present my way of reasoning here.

Let’s start with lifting. Lifting in functional programming is defined as an operation that transforms a function f: A => B into a function f': M[A] -> M[B], where M[_] is a functor. A functor M[A], in simple words, is just a wrapper around a value of type A with addition of extra map operation that can be used to transform the wrapped value. A lot of types that we are using in our daily practice as programmers like Option[T], Try[T] or Future[T] are functors. Because Scala does not define Functor[T] trait we cannot write a generic lift function that could work with all these types. Instead we must write specific lift functions that work with only single functor type e.g.

def liftOption[A, B](f: A => B): Option[A] => Option[B] = {
  optA => optA.map(f)
}

We can use liftOption to e.g. make negate function work with Option[Int] argument:

def negate(a: Int): Int = -a

val negateOpt = liftOption(negate)

println(negateOpt(Some(1)))
println(negateOpt(None))
// prints:
//  Some(-1)
//  None

Similarly to liftOption we could define liftFuture:

def liftFuture[A,B](f: A => B)
                  (implicit ec: ExecutionContext)
: Future[A] => Future[B] = {
    futA => futA.map(f)
}

And then we can use it on a function that returns Unit to get an async function that, no surprise here, will return Future[Unit]:

import ExecutionContext.Implicits.global

def printIt(s: String): Unit = println(s)
val printItF = liftFuture(printIt)

val r: Unit = Await.result(
    printItF(Future.successful("ok")),
    duration.Duration.Inf)

But of course my colleague had his own arguments. He pointed out that e.g. ExecutorService submit method returns Future<?>, not Future<Void> as my reasoning would suggest. So there must be a good reason why library designers chosen Future<?>, he continued, but when asked what exactly this reason could be he was not able to answer. But he counter my question arguing that the standard library establishes good patterns and practices, so if they use Future<?> then we should do the same in our code. Future is there

My colleague had a point. I needed to research the subject: why Future<?> and not Future<Void>? After a bit of googling I noticed that runAsync method of CompletableFuture does not follow the Future<?> convention and instead returns CompletableFuture<Void>. So it looks like the library designers changed their mind and now prefer returning Future<Void>: Void is there

I pointed this out to my colleague, but he countered by saying that Future[Unit] is just uncomfortable to use. Consider this example he said:

def findIdByName(name: String): Future[Int] = {
  Future.successful(123)
}

def deleteRecord(id: Int): Future[Int] = {
  Future.successful(1)
}

def doStuff(name: String): Future[Unit] = {
  findIdByName(name)
    .flatMap(id => deleteRecord(id))
    .map(_ => ())
}

Look at this ugly .map(_ => ()) he said, it’s awful, isn’t it? I pointed out that he can use for-comprehensions to get rid of the last map, but may argument was weak - the fluent interface is often nicer and faster to work with.

def doStuff(name: String): Future[Unit] = {
  for {
    id <- findIdByName(name)
    _ <- deleteRecord(id)
  } yield ()
}

Indeed I find it very strange that Scala does not provide a conversion from Future[T] to Future[Unit] out of the box. In sync code we could write:

def doStuff(name: String): Future[Unit] = {
  val id <- findIdByName(name)
  deleteRecord(id)
}

and the value returned by deleteRecord would be automatically discarded. Why can’t we have something similar in async code?

Yet this last problem pointed me to the following issue. If we agree to use Future[_] everywhere, then we may return by mistake a value that should never be returned. In other words we may inadvertently break object or module encapsulation. There is also a small risk of a memory leak, if we return say a huge array and the Future[_] value will be kept for a while. This is illustrated by the following example:

// In some object
private var balance = 1234

private def increaseBalance(amount: Int): Future[Int] = {
  val oldBalance = balance
  balance = oldBalance + amount
  Future.successful(oldBalance)
}

// This should return nothing
def doStuff(): Future[_] = {
  increaseBalance(100)
}

// Calling code
val escapedBalance = Await.result(obj.doStuff(), duration.Duration.Inf)
println(escapedBalance) // 1234

Seeing all these arguments, in the end, we decided to go with Future[Unit] although it is not a perfect solution.

There is nothing better than a healthy debate BTW ;D