Search code examples
scalaadtfor-comprehensioneither

ADT vs Either vs Exceptions


So, the current implementation uses twitter's Future along with throwing exceptions to signal invalid use-case along with for-comprehensions, like so:

def someMethod(a: ...): Future[X] = {
  // do something
  // if something goes wrong throw exception
  throw new Exception("Certificate not issued")
}

// in some other method, where exceptions break the loop and exit
def someOtherMethod(a: ...): Future[Y] = {
  for {
    x <- someMethod(...)
    y <- yetAnotherMethod(...) // which throws another exception
  } yield y
}

The general idea being, when something goes wrong, an exception gets thrown, which will cause exit from the for-comprehension block. I want to get away from throwing exceptions. One way to solve it is, returning Either[Error, X], and the other way ADT using sealed trait. So, instead of throwing an Exception you can return Left(Error) or an ADT like case object NoCertificate extends CertificateResponse.

Question is: Can I keep the existing for loops intact, if I replace the methods which currently has throw Exception with Either or ADT?

For sake of completeness, here's how I would code my Either and ADT:

sealed trait Error
case object CertificateError extends Error
case object SomeOtherError extends Error

def someMethod(a: ...): Future[Either[Error, CertificateResponse]] = {
  // returns Left(CertificateError) or Right(CertificateResponse)
}

OR

sealed trait CertificateResponse
case class Certificate(x509) extends CertificateResponse
case object NoCertificate extends CertificateResponse

def someMethod(a: ...): Future[CertificateResponse] = {
  // returns NoCertificate or Certificate(X509)
}

will either of these alternative solution (to throwing exceptions and breaking referential transparency), work with for-comprehensions? Will the negative response: Left() or NoCertificate automagically exit the for-comprehension block? If not, how to make it, such that I can keep the for-comprehension blocks as is? Something akin to cats EitherT's leftMap?

Please Note: We cannot use cats Monad Transformer like EitherT (which has leftMap which signals exit conditions), as that is not one of the libraries we use in our stack. Sorry!

Thanks!


Solution

  • As mentioned in my comment, I'd really look into whether some library offering a monad transformer is a possibility (Scalaz also includes one), because this is exactly the use case they're for. If it's really not possible, your only alternative is writing your own - meaning, create some class which can wrap your method outputs that has map and flatMap methods that do what you want. This is doable for both the Either and the ADT-based solution. The Either-based one would look a little like this:

    sealed trait Error
    
    case object CertificateError extends Error
    case object SomeOtherError extends Error
    
    case class Result[+T](value: Future[Either[Error, T]]) {
      def map[S](f: T => S)(implicit ec: ExecutionContext) : Result[S] = {
        Result(value.map(_.map(f)))
      }
    
      def flatMap[S](f: T => Result[S])(implicit ec: ExecutionContext) : Result[S] = {
        Result {
          value.flatMap {
            case Left(error) => Future.successful(Left(error))
            case Right(result) => f(result).value
          }
        }
      }
    }
    

    (You 100% need this sort of wrapping class! There's no way of getting a return type Future[ADT] or Future[Either[Error, Result]] to behave in the way you want because it would require altering the way Future works.)

    With the code as above, you can use for-comprehensions over Result types and they'll automagically exit if either their containing Future fails or the Future succeeds with an Error as you specified. Silly example:

    import ExecutionContext.Implicits.global
    import scala.concurrent.{Await, Future}
    import scala.concurrent.duration._
    
    def getZero() : Result[Int] = Result(Future.successful(Right(0)))
    
    def error() : Result[Unit] = Result(Future.successful(Left(SomeOtherError)))
    
    def addTwo(int: Int) : Result[Int] = Result(Future.successful(Right(2 + int)))
    
    val result = for {
      zero <- getZero()
      _ <- error()
      two <- addTwo(zero)
    } yield two
    
    Await.result(result.value, 10.seconds) // will be `Left(SomeOtherError)`
    

    The reason I strongly recommend an existing EitherT transformer is because they come with a ton of utility methods and logic that will make your life significantly easier, but if it's not an option, it's not an option.