How to work with a an Future which wrappes an Either?

Viewed 208

In my current project, I used Either[Result, HandbookModule] (Result is an HTTP Statuscode) as a return type so that I can create the correct status when something goes wrong. I have now refactored my database access to be non-blocking.

This change requires that my return type for database access functions changed to Future[Either[Result, HandbookModule]].

Now I am not sure on how to glue this function together with another function which returns Either[Result, Long].

So to better illustrate what I mean:

def moduleDao.getHandbooks(offset, limit): Future[Either[Result, List[Module]] = Future(Right(List(Module(1))))

def nextOffset(offset, limit, results): Either[_, Long] = Right(1)

def getHandbooks(
  offset: Long,
  limit: Long): Future[Either[Result, (List[HandbookModule], Long)]] = {
  for {
    results <- moduleDao.getHandbooks(offset, limit)
    offset  <- nextOffset(offset, limit, results)
  } yield (results, offset)
}

Before the change, this was obviously no problem but I don't know what would be the best approach.

Or is there a way to convert a Future[Either[A, B]] to an Either[A, Future[B]]?

3 Answers

In order to unwrap your method from the Future, you'd have to block it and wait for the result. You could do it using Await.result.

But blocking the future is usually not considered to be the best practice. More on this here.

So you should tackle that problem differently. What you're facing is actually common issue with nested monad stacks and it can be handled with monad transformers.

Scala's functional programming library cats provides an implementation of EitherT monad transformer.

In your case, you could use EitherT.apply to transform Future[Either[Result, List[Module]] into EitherT[Future, Result, List[Module]] and EitherT.fromEither to lift Either[_, Long].

It could look like this:

import cats.data.EitherT
import cats.implicits._
  
def getHandbooks(
   offset: Long,
   limit: Long
): Future[Either[String, (List[String], Long)]] = {
  val result: EitherT[Future, String, (List[String], Long)] = for {
    results <- EitherT(moduleDao.getHandbooks(offset, limit))
    offset  <- EitherT.fromEither[Future](nextOffset(offset, limit, results))
  } yield (results, offset)

  result.value //unwrap result from EitherT
}

I had to make a number of assumptions and adjustments/corrections to the posted code in order to make it usable. (You don't make it easy for those who want to help you.)

If you can tolerate a default Long value, when nextOffset() returns Left instead of Right[Long], then this appears to type-check and compile.

def getHandbooks(offset: Long
                ,limit : Long
                ): Future[Either[Result, (List[Module], Long)]] =
  moduleDao.getHandbooks(offset,limit).map(_.map(ms => 
    (ms, nextOffset(offset,limit,ms).getOrElse(0L))))

If I understand correctly, the Left side of you Either represents the error state, correct?

If so, I think you should refactor your API to not use Either, and simply use a failed Future to represent the error state. Something along the lines of:

// Custom exception that wraps existing Result
case class MyCustomException(result: Result) extends Exception

class ModuleDao {
  ...
  def getHandbooks(offset, limit): Future[List[Module] = {
    // You'd probably want to do this asynchronously
    // But for demonstration purposes
    val origRetVal: Either[Result, List[Module] = ??? // current code returning your Either
    origRetVal match {
      case Right(modules: List[Module]) =>
        Future.successful(modules)
      case Left(result: Result) =>
        // Failed future wrapping custom exception
        Future.failed(MyCustomException(result))
    }
  }
  ...
}
Related