0

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!

iyerland
  • 632
  • 2
  • 10
  • 24
  • Scalaz also includes an `EitherT`, is that library an option? Because really, the behaviour you're specifying is _precisely_ what using a monad transformer gets you, and if you can't use libraries then you'll have to code one by hand. – Astrid Aug 15 '19 at 09:06

1 Answers1

0

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.

Astrid
  • 1,808
  • 12
  • 24