问题
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?
回答1:
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
回答2:
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.
回答3:
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.
来源:https://stackoverflow.com/questions/54931204/recovering-underlying-future-into-cats-eitherts-left