If I have a Future[Either[String, Int]]
that represents either a possible error message (String
) or a successful computation (Int
), it is simple to move the Future
's potential failure into the left side as an error message:
def handleFailure(fe: Future[Either[String,Int]]) =
f.recover({ case e: Exception => Left(s"failed because ${e.getMessage}"))
I would expect something similar to exist for EitherT
, but maybe I just can't find out what it is called. It is relatively simple, but involves unboxing and re-boxing the EitherT which feels kludgey:
def handleFailureT(fe: EitherT[Future, String, Int]) =
EitherT(handleFailure(et.value)) // See above for handleFailure definition
Cats did add a MonadError
instance a while ago, but it's specifically for recovering straight into the Either's right
, not for replacing the Either itself.
Is handleFailureT
implemented it Cats, and if so what is it called?
Ideally it would look something like this:
class EitherT[F, A, B] {
def handleFailureT[AA](f: PartialFunction[Throwable, AA]): EitherT[F, AA, B] = EitherT(value.recover(f))
}
// Then used like:
val et: EitherT[Future, String, Int] =
EitherT(Future(throw new Exception("an error message"))
et.handleFailure {
case e: Exception => s"Got error with message '${e.message}'"
}
// Now et is Left("Got error with message 'an error message'")
It is not at all obvious, but I think this is what attempt
and attemptT
are for. For example:
val myTry: Try[Int] = Try(2)
val myFuture: Future[String] = Future.failed(new Exception())
val myTryET: EitherT[Try, Throwable, Int] = myTry.attemptT
val myFutureET: EitherT[Future, Throwable, String] = myFuture.attemptT
// alternatively
val myFutureET: EitherT[Future, Throwable, String] = EitherT(myFuture.attempt)
There was a PR to add this to the documentation: https://github.com/typelevel/cats/pull/3178 -- but it doesn't appear in the documentation currently. However, you can see it here: https://github.com/typelevel/cats/blob/master/docs/src/main/tut/datatypes/eithert.md#from-applicativeerrorf-e-to-eithertf-e-a
After spending several hours on this I am fairly certain that, as of March 2019, this function is not implemented in cats directly. However, the already existing catsDataMonadErrorFForEitherT
monad does make it possible to implement it in a mostly uncomplicated manner.
implicit class EitherTFutureAdditions[A, B](et: EitherT[Future, A, B]) {
val me = EitherT.catsDataMonadErrorFForEitherT[Future, Throwable, A]
def recoverLeft(pf: PartialFunction[Throwable, A]): EitherT[Future, A, B] =
me.recoverWith[B](et) { case t: Throwable =>
EitherT.fromEither[Future](Left(pf(t)))
}
}
I am uncertain what the performance implications of constructing the monad within the generic implicit class are, but it works. If you don't need the generic case, you may want to replace [A, B]
with explicit types.
While I was at it I also wrote recoverWithFlat
, handleErrorLeft
, and handleErrorWithFlat
and packaged it all into a file EitherTUtils.scala
// Place this in a new file and then use it like so:
//
// import EitherTUtils.EitherTFutureAdditions
//
// val et: EitherT[Future, String, Int] =
// EitherT(Future.failed[Either[String, Int]](new Exception("example")))
// et recoverLeft {
// case e: Exception => s"Failed with reason ${e.getMessage}"
// }
//
object EitherTUtils {
/**
* Convenience additions for recovering and handling Future.failed within an EitherT
*
* @see [[cats.ApplicativeError]] for recover, recoverWith, handleError, handleErrorWith, and attemptT
*
* @param et a Futured EitherT
* @tparam A the Either's left type
* @tparam B the Either's right type
*/
implicit class EitherTFutureAdditions[A, B](et: EitherT[Future, A, B]) {
val me = EitherT.catsDataMonadErrorFForEitherT[Future, Throwable, A]
/**
* Recover from certain errors from this EitherT's Future (if failed) by mapping them to the EitherT's
* left value.
*
* @see [[recoverWithFlat]] for mapping to an Either[Future, A, B]
*
* @see [[handleErrorWithFlat]] to handle any/all errors.
*/
def recoverLeft(pf: PartialFunction[Throwable, A]): EitherT[Future, A, B] =
me.recoverWith[B](et) {
case t: Throwable =>
EitherT.fromEither[Future](Left(pf(t)))
}
/**
* Recover from certain errors from this EitherT's Future (if failed) by mapping them to the EitherT's
* value.
*
* @see [[recoverLeft]] for mapping to an EitherT's left value.
*
* @see [[handleErrorWithFlat]] to handle any/all errors.
*/
def recoverWithFlat(pf: PartialFunction[Throwable, Either[A, B]]): EitherT[Future, A, B] =
me.recoverWith[B](et) {
case t: Throwable =>
EitherT.fromEither[Future](pf(t))
}
/**
* Handle any error from this EitherT's Future (if failed) by mapping them to the EitherT's left value.
*
* @see [[recoverWithFlat]] for handling only certain errors
*
* @see [[handleErrorLeft]] for mapping to the EitherT's left value
*/
def handleErrorLeft(pf: PartialFunction[Throwable, A]): EitherT[Future, A, B] =
me.handleErrorWith[B](et) { t =>
EitherT.fromEither[Future](Left[A, B](pf(t)))
}
/**
* Handle any error from this EitherT's Future (if failed) by mapping them to the EitherT's value.
*
* @see [[recoverWithFlat]] for handling only certain errors
*
* @see [[handleErrorLeft]] for mapping to the EitherT's left value
*/
def handleErrorWithFlat(pf: PartialFunction[Throwable, Either[A, B]]): EitherT[Future, A, B] =
me.handleErrorWith[B](et) { t =>
EitherT.fromEither[Future](pf(t))
}
}
}
I thought that these might be my first contribution to cats, but after several hours of navigating the library's layout I realized the modifications would be non trivial and I don't have the knowledge level yet to submit them in a manner that wouldn't require significant work from other project contributors.
I may try again once I better understand the cats library structure.
Here is a generalized version of your EitherTUtils
:
import cats.data.EitherT
object EitherTUtils {
implicit class EitherTRecoverErrors[F[_], A, B, E](et: EitherT[F, A, B])(implicit me: MonadError[F, E]) {
type FE[X] = EitherT[F, A, X]
implicit val ME: MonadError[FE, E] = implicitly
def recoverLeft(pf: PartialFunction[E, A]): EitherT[F, A, B] =
ME.recoverWith(et)(pf.andThen(EitherT.leftT(_)))
def recoverWithFlat(pf: PartialFunction[E, Either[A, B]]): EitherT[F, A, B] =
ME.recoverWith(et)(pf.andThen(EitherT.fromEither(_)))
def handleErrorLeft(f: E => A): EitherT[F, A, B] =
ME.handleErrorWith(et)(f.andThen(EitherT.leftT(_)))
def handleErrorWithFlat(f: E => Either[A, B]): EitherT[F, A, B] =
ME.handleErrorWith(et)(f.andThen(EitherT.fromEither(_)))
}
}
object Usage {
import EitherTUtils._
import cats.implicits._
import scala.concurrent.ExecutionContext.Implicits.global
val e: EitherT[Future, String, Int] = EitherT.liftF(Future.failed(new RuntimeException)).recoverLeft {
case e: IllegalStateException =>
e.getMessage
}
}
I agree cats could make it easier to work with "failed" EitherTs, hopefully we see something like this in future versions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With