Recovering underlying Future into Cats' EitherT's Left?

独自空忆成欢 提交于 2020-12-29 12:33:46

问题


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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!