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!
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.