Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recovering underlying Future into Cats' EitherT's Left?

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'")
like image 782
Cory Klein Avatar asked Feb 28 '19 17:02

Cory Klein


3 Answers

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

like image 184
kflorence Avatar answered Nov 03 '22 20:11

kflorence


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.

like image 42
Cory Klein Avatar answered Nov 03 '22 18:11

Cory Klein


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.

like image 1
Wogan Avatar answered Nov 03 '22 20:11

Wogan