diff --git a/core/src/main/scala/cats/instances/future.scala b/core/src/main/scala/cats/instances/future.scala index 0518529ed0..65f81b84cb 100644 --- a/core/src/main/scala/cats/instances/future.scala +++ b/core/src/main/scala/cats/instances/future.scala @@ -1,8 +1,8 @@ package cats package instances -import scala.util.control.NonFatal import scala.concurrent.{ExecutionContext, Future} +import scala.util.control.NonFatal trait FutureInstances extends FutureInstances1 { @@ -10,28 +10,38 @@ trait FutureInstances extends FutureInstances1 { implicit ec: ExecutionContext ): MonadError[Future, Throwable] with CoflatMap[Future] with Monad[Future] = new FutureCoflatMap with MonadError[Future, Throwable] with Monad[Future] with StackSafeMonad[Future] { - def pure[A](x: A): Future[A] = Future.successful(x) + def pure[A](x: A): Future[A] = + Future.successful(x) + + def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = + fa.flatMap(f) - def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = fa.flatMap(f) + def handleErrorWith[A](fea: Future[A])(f: Throwable => Future[A]): Future[A] = + fea.recoverWith { case t => f(t) } - def handleErrorWith[A](fea: Future[A])(f: Throwable => Future[A]): Future[A] = fea.recoverWith { case t => f(t) } + def raiseError[A](e: Throwable): Future[A] = + Future.failed(e) - def raiseError[A](e: Throwable): Future[A] = Future.failed(e) - override def handleError[A](fea: Future[A])(f: Throwable => A): Future[A] = fea.recover { case t => f(t) } + override def handleError[A](fea: Future[A])(f: Throwable => A): Future[A] = + fea.recover { case t => f(t) } override def attempt[A](fa: Future[A]): Future[Either[Throwable, A]] = (fa.map(a => Right[Throwable, A](a))).recover { case NonFatal(t) => Left(t) } - override def recover[A](fa: Future[A])(pf: PartialFunction[Throwable, A]): Future[A] = fa.recover(pf) + override def recover[A](fa: Future[A])(pf: PartialFunction[Throwable, A]): Future[A] = + fa.recover(pf) override def recoverWith[A](fa: Future[A])(pf: PartialFunction[Throwable, Future[A]]): Future[A] = fa.recoverWith(pf) - override def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f) + override def map[A, B](fa: Future[A])(f: A => B): Future[B] = + fa.map(f) - override def catchNonFatal[A](a: => A)(implicit ev: Throwable <:< Throwable): Future[A] = Future(a) + override def catchNonFatal[A](a: => A)(implicit ev: Throwable <:< Throwable): Future[A] = + Future(a) - override def catchNonFatalEval[A](a: Eval[A])(implicit ev: Throwable <:< Throwable): Future[A] = Future(a.value) + override def catchNonFatalEval[A](a: Eval[A])(implicit ev: Throwable <:< Throwable): Future[A] = + Future(a.value) } } diff --git a/core/src/main/scala/cats/instances/try.scala b/core/src/main/scala/cats/instances/try.scala index 8816d47e3c..5ec80f0193 100644 --- a/core/src/main/scala/cats/instances/try.scala +++ b/core/src/main/scala/cats/instances/try.scala @@ -1,11 +1,10 @@ package cats package instances -import TryInstances.castFailure +import cats.instances.TryInstances.castFailure -import scala.util.control.NonFatal -import scala.util.{Failure, Success, Try} import scala.annotation.tailrec +import scala.util.{Failure, Success, Try} trait TryInstances extends TryInstances1 { @@ -69,7 +68,7 @@ trait TryInstances extends TryInstances1 { ta.recover { case t => f(t) } override def attempt[A](ta: Try[A]): Try[Either[Throwable, A]] = - (ta.map(a => Right[Throwable, A](a))).recover { case NonFatal(t) => Left(t) } + ta match { case Success(a) => Success(Right(a)); case Failure(e) => Success(Left(e)) } override def recover[A](ta: Try[A])(pf: PartialFunction[Throwable, A]): Try[A] = ta.recover(pf) diff --git a/core/src/main/scala/cats/syntax/applicativeError.scala b/core/src/main/scala/cats/syntax/applicativeError.scala index 353d739b10..6657729c52 100644 --- a/core/src/main/scala/cats/syntax/applicativeError.scala +++ b/core/src/main/scala/cats/syntax/applicativeError.scala @@ -100,6 +100,35 @@ final class ApplicativeErrorOps[F[_], E, A](private val fa: F[A]) extends AnyVal def recoverWith(pf: PartialFunction[E, F[A]])(implicit F: ApplicativeError[F, E]): F[A] = F.recoverWith(fa)(pf) + /** + * Returns a new value that transforms the result of the source, + * given the `recover` or `map` functions, which get executed depending + * on whether the result is successful or if it ends in error. + * + * This is an optimization on usage of [[attempt]] and [[Functor.map]], + * this equivalence being available: + * + * {{{ + * fa.redeem(fe, fs) <-> fa.attempt.map(_.fold(fe, fs)) + * }}} + * + * Usage of `redeem` subsumes [[handleError]] because: + * + * {{{ + * fa.redeem(fe, id) <-> fa.handleError(fe) + * }}} + * + * + * @see [[MonadErrorOps.redeemWith]], [[attempt]] and [[handleError]] + * + * @param recover is the function that gets called to recover the source + * in case of error + * @param f is the function that gets to transform the source + * in case of success + */ + def redeem[B](recover: E => B, f: A => B)(implicit F: ApplicativeError[F, E]): F[B] = + F.handleError(F.map(fa)(f))(recover) + def onError(pf: PartialFunction[E, F[Unit]])(implicit F: ApplicativeError[F, E]): F[A] = F.onError(fa)(pf) diff --git a/core/src/main/scala/cats/syntax/monadError.scala b/core/src/main/scala/cats/syntax/monadError.scala index f6964dfeb0..aac82880ff 100644 --- a/core/src/main/scala/cats/syntax/monadError.scala +++ b/core/src/main/scala/cats/syntax/monadError.scala @@ -29,6 +29,40 @@ final class MonadErrorOps[F[_], E, A](private val fa: F[A]) extends AnyVal { def adaptError(pf: PartialFunction[E, E])(implicit F: MonadError[F, E]): F[A] = F.adaptError(fa)(pf) + + /** + * Returns a new value that transforms the result of the source, + * given the `recover` or `bind` functions, which get executed depending + * on whether the result is successful or if it ends in error. + * + * This is an optimization on usage of [[ApplicativeError.attempt]] and [[FlatMap.flatMap]], + * this equivalence being available: + * + * {{{ + * fa.redeemWith(fe, fs) <-> fa.attempt.flatMap(_.fold(fe, fs)) + * }}} + * + * Usage of `redeemWith` subsumes [[ApplicativeError.handleErrorWith]] because: + * + * {{{ + * fa.redeemWith(fe, F.pure) <-> fa.handleErrorWith(fe) + * }}} + * + * Usage of `redeemWith` also subsumes [[FlatMap.flatMap]] because: + * + * {{{ + * fa.redeemWith(F.raiseError, fs) <-> fa.flatMap(fs) + * }}} + * + * @see [[ApplicativeErrorOps.redeem]], [[ApplicativeError.attempt]] and [[ApplicativeError.handleErrorWith]] + * + * @param recover is the function that gets called to recover the source + * in case of error + * @param bind is the function that gets to transform the source + * in case of success + */ + def redeemWith[B](recover: E => F[B], bind: A => F[B])(implicit F: MonadError[F, E]): F[B] = + F.flatMap(F.attempt(fa))(_.fold(recover, bind)) } final class MonadErrorRethrowOps[F[_], E, A](private val fea: F[Either[E, A]]) extends AnyVal { diff --git a/tests/src/test/scala/cats/tests/SyntaxSuite.scala b/tests/src/test/scala/cats/tests/SyntaxSuite.scala index 2752af4f14..39804cbc7d 100644 --- a/tests/src/test/scala/cats/tests/SyntaxSuite.scala +++ b/tests/src/test/scala/cats/tests/SyntaxSuite.scala @@ -381,7 +381,7 @@ object SyntaxSuite val done = a.tailRecM[F, B](a => returnValue) } - def testApplicativeError[F[_, _], E, A](implicit F: ApplicativeError[F[E, *], E]): Unit = { + def testApplicativeError[F[_, _], E, A, B](implicit F: ApplicativeError[F[E, *], E]): Unit = { type G[X] = F[E, X] val e = mock[E] @@ -404,12 +404,49 @@ object SyntaxSuite val pfegea = mock[PartialFunction[E, G[A]]] val gea4 = ga.recoverWith(pfegea) + + val eb = mock[E => B] + val ab = mock[A => B] + val gb: G[B] = gea.redeem(eb, ab) } def testApplicativeErrorSubtype[F[_], A](implicit F: ApplicativeError[F, CharSequence]): Unit = { val fea = "meow".raiseError[F, A] } + def testMonadError[F[_, _], E, A, B](implicit F: MonadError[F[E, *], E]): Unit = { + type G[X] = F[E, X] + + val e = mock[E] + val ga = e.raiseError[G, A] + + val gea = mock[G[A]] + + val ea = mock[E => A] + val gea1 = ga.handleError(ea) + + val egea = mock[E => G[A]] + val gea2 = ga.handleErrorWith(egea) + + val gxea = ga.attempt + + val gxtea = ga.attemptT + + val pfea = mock[PartialFunction[E, A]] + val gea3 = ga.recover(pfea) + + val pfegea = mock[PartialFunction[E, G[A]]] + val gea4 = ga.recoverWith(pfegea) + + val eb = mock[E => B] + val ab = mock[A => B] + val gb: G[B] = gea.redeem(eb, ab) + + val efb = mock[E => G[B]] + val afb = mock[A => G[B]] + val gb2: G[B] = gea.redeemWith(efb, afb) + } + def testNested[F[_], G[_], A]: Unit = { val fga: F[G[A]] = mock[F[G[A]]]