diff --git a/core/src/main/scala/cats/ApplicativeError.scala b/core/src/main/scala/cats/ApplicativeError.scala index ec4ab1bcda..7831e42677 100644 --- a/core/src/main/scala/cats/ApplicativeError.scala +++ b/core/src/main/scala/cats/ApplicativeError.scala @@ -103,6 +103,38 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { def recoverWith[A](fa: F[A])(pf: PartialFunction[E, F[A]]): F[A] = handleErrorWith(fa)(e => pf.applyOrElse(e, raiseError)) + /** + * 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 [[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) + * }}} + * + * Implementations are free to override it in order to optimize + * error recovery. + * + * @see [[MonadError.redeemWith]], [[attempt]] and [[handleError]] + * + * @param fa is the source whose result is going to get transformed + * @param recover is the function that gets called to recover the source + * in case of error + * @param map is the function that gets to transform the source + * in case of success + */ + def redeem[A, B](fa: F[A])(recover: E => B, f: A => B): F[B] = + handleError(map(fa)(f))(recover) + /** * Execute a callback on certain errors, then rethrow them. * Any non matching error is rethrown as well. diff --git a/core/src/main/scala/cats/MonadError.scala b/core/src/main/scala/cats/MonadError.scala index 3adb13f8f9..2d5eeb5a3c 100644 --- a/core/src/main/scala/cats/MonadError.scala +++ b/core/src/main/scala/cats/MonadError.scala @@ -65,6 +65,44 @@ trait MonadError[F[_], E] extends ApplicativeError[F, E] with Monad[F] { */ def rethrow[A, EE <: E](fa: F[Either[EE, A]]): F[A] = flatMap(fa)(_.fold(raiseError, pure)) + + /** + * 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 [[attempt]] and [[flatMap]], + * this equivalence being available: + * + * {{{ + * fa.redeemWith(fe, fs) <-> fa.attempt.flatMap(_.fold(fe, fs)) + * }}} + * + * Usage of `redeemWith` subsumes [[handleErrorWith]] because: + * + * {{{ + * fa.redeemWith(fe, F.pure) <-> fa.handleErrorWith(fe) + * }}} + * + * Usage of `redeemWith` also subsumes [[flatMap]] because: + * + * {{{ + * fa.redeemWith(F.raiseError, fs) <-> fa.flatMap(fs) + * }}} + * + * Implementations are free to override it in order to optimize + * error recovery. + * + * @see [[redeem]], [[attempt]] and [[handleErrorWith]] + * + * @param fa is the source whose result is going to get transformed + * @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[A, B](fa: F[A])(recover: E => F[B], bind: A => F[B]): F[B] = + flatMap(attempt(fa))(_.fold(recover, bind)) } object MonadError { diff --git a/core/src/main/scala/cats/instances/either.scala b/core/src/main/scala/cats/instances/either.scala index 18ba15f413..d156458705 100644 --- a/core/src/main/scala/cats/instances/either.scala +++ b/core/src/main/scala/cats/instances/either.scala @@ -98,6 +98,13 @@ trait EitherInstances extends cats.kernel.instances.EitherInstances { override def recoverWith[B](fab: Either[A, B])(pf: PartialFunction[A, Either[A, B]]): Either[A, B] = fab.recoverWith(pf) + override def redeem[B, R](fab: Either[A, B])(recover: A => R, map: B => R): Either[A, R] = + Right(fab.fold(recover, map)) + + override def redeemWith[B, R](fab: Either[A, B])(recover: A => Either[A, R], + bind: B => Either[A, R]): Either[A, R] = + fab.fold(recover, bind) + override def fromEither[B](fab: Either[A, B]): Either[A, B] = fab diff --git a/core/src/main/scala/cats/instances/future.scala b/core/src/main/scala/cats/instances/future.scala index 0518529ed0..b89b7b81cc 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.{Failure, Success} trait FutureInstances extends FutureInstances1 { @@ -10,28 +10,41 @@ 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 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 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 pure[A](x: A): Future[A] = + Future.successful(x) + override def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = + fa.flatMap(f) + override def handleErrorWith[A](fea: Future[A])(f: Throwable => Future[A]): Future[A] = + fea.recoverWith { case t => f(t) } + override 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 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) - + fa.transformWith( + r => + Future.successful( + r match { + case Success(a) => Right(a) + case Failure(e) => Left(e) + } + ) + ) + override def redeemWith[A, B](fa: Future[A])(recover: Throwable => Future[B], bind: A => Future[B]): Future[B] = + fa.transformWith { + case Success(a) => bind(a) + case Failure(e) => recover(e) + } + 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 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 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 catchNonFatalEval[A](a: Eval[A])(implicit ev: Throwable <:< Throwable): Future[A] = + Future(a.value) } } diff --git a/core/src/main/scala/cats/instances/option.scala b/core/src/main/scala/cats/instances/option.scala index cc4961103e..44d8f0bd7a 100644 --- a/core/src/main/scala/cats/instances/option.scala +++ b/core/src/main/scala/cats/instances/option.scala @@ -67,6 +67,20 @@ trait OptionInstances extends cats.kernel.instances.OptionInstances { def handleErrorWith[A](fa: Option[A])(f: (Unit) => Option[A]): Option[A] = fa.orElse(f(())) + override def redeem[A, B](fa: Option[A])(recover: Unit => B, map: A => B): Option[B] = + fa match { + case Some(a) => Some(map(a)) + // N.B. not pattern matching `case None` on purpose + case _ => Some(recover(())) + } + + override def redeemWith[A, B](fa: Option[A])(recover: Unit => Option[B], bind: A => Option[B]): Option[B] = + fa match { + case Some(a) => bind(a) + // N.B. not pattern matching `case None` on purpose + case _ => recover(()) + } + def traverse[G[_]: Applicative, A, B](fa: Option[A])(f: A => G[B]): G[Option[B]] = fa match { case None => Applicative[G].pure(None) diff --git a/core/src/main/scala/cats/instances/try.scala b/core/src/main/scala/cats/instances/try.scala index 8816d47e3c..b1b571b006 100644 --- a/core/src/main/scala/cats/instances/try.scala +++ b/core/src/main/scala/cats/instances/try.scala @@ -69,7 +69,15 @@ 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 redeem[A, B](ta: Try[A])(recover: Throwable => B, map: A => B): Try[B] = + ta match { case Success(a) => Try(map(a)); case Failure(e) => Try(recover(e)) } + + override def redeemWith[A, B](ta: Try[A])(recover: Throwable => Try[B], bind: A => Try[B]): Try[B] = + try ta match { + case Success(a) => bind(a); case Failure(e) => recover(e) + } catch { case NonFatal(e) => Failure(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 ed58a5cc2a..87d5e782f8 100644 --- a/core/src/main/scala/cats/syntax/applicativeError.scala +++ b/core/src/main/scala/cats/syntax/applicativeError.scala @@ -97,6 +97,9 @@ 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) + def redeem[B](recover: E => B, f: A => B)(implicit F: ApplicativeError[F, E]): F[B] = + F.redeem[A, B](fa)(recover, f) + 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..7d6f152ce3 100644 --- a/core/src/main/scala/cats/syntax/monadError.scala +++ b/core/src/main/scala/cats/syntax/monadError.scala @@ -29,6 +29,9 @@ 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) + + def redeemWith[B](recover: E => F[B], bind: A => F[B])(implicit F: MonadError[F, E]): F[B] = + F.redeemWith[A, B](fa)(recover, bind) } final class MonadErrorRethrowOps[F[_], E, A](private val fea: F[Either[E, A]]) extends AnyVal { diff --git a/laws/src/main/scala/cats/laws/ApplicativeErrorLaws.scala b/laws/src/main/scala/cats/laws/ApplicativeErrorLaws.scala index b0740ecb5f..fd013ed6b0 100644 --- a/laws/src/main/scala/cats/laws/ApplicativeErrorLaws.scala +++ b/laws/src/main/scala/cats/laws/ApplicativeErrorLaws.scala @@ -45,6 +45,9 @@ trait ApplicativeErrorLaws[F[_], E] extends ApplicativeLaws[F] { def onErrorRaise[A](fa: F[A], e: E, fb: F[Unit]): IsEq[F[A]] = F.onError(F.raiseError[A](e)) { case err => fb } <-> F.map2(fb, F.raiseError[A](e))((_, b) => b) + + def redeemDerivedFromAttemptMap[A, B](fa: F[A], fe: E => B, fs: A => B): IsEq[F[B]] = + F.redeem(fa)(fe, fs) <-> F.map(F.attempt(fa))(_.fold(fe, fs)) } object ApplicativeErrorLaws { diff --git a/laws/src/main/scala/cats/laws/MonadErrorLaws.scala b/laws/src/main/scala/cats/laws/MonadErrorLaws.scala index 6912ffd88a..c8001e5b32 100644 --- a/laws/src/main/scala/cats/laws/MonadErrorLaws.scala +++ b/laws/src/main/scala/cats/laws/MonadErrorLaws.scala @@ -22,6 +22,9 @@ trait MonadErrorLaws[F[_], E] extends ApplicativeErrorLaws[F, E] with MonadLaws[ def rethrowAttempt[A](fa: F[A]): IsEq[F[A]] = F.rethrow(F.attempt(fa)) <-> fa + + def redeemWithDerivedFromAttemptFlatMap[A, B](fa: F[A], fe: E => F[B], fs: A => F[B]): IsEq[F[B]] = + F.redeemWith(fa)(fe, fs) <-> F.flatMap(F.attempt(fa))(_.fold(fe, fs)) } object MonadErrorLaws { diff --git a/laws/src/main/scala/cats/laws/discipline/ApplicativeErrorTests.scala b/laws/src/main/scala/cats/laws/discipline/ApplicativeErrorTests.scala index e417b0f5de..85636ac363 100644 --- a/laws/src/main/scala/cats/laws/discipline/ApplicativeErrorTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/ApplicativeErrorTests.scala @@ -53,7 +53,8 @@ trait ApplicativeErrorTests[F[_], E] extends ApplicativeTests[F] { laws.attemptFromEitherConsistentWithPure[A] _ ), "applicativeError onError pure" -> forAll(laws.onErrorPure[A] _), - "applicativeError onError raise" -> forAll(laws.onErrorRaise[A] _) + "applicativeError onError raise" -> forAll(laws.onErrorRaise[A] _), + "monadError redeem is derived from attempt and map" -> forAll(laws.redeemDerivedFromAttemptMap[A, B] _) ) } } diff --git a/laws/src/main/scala/cats/laws/discipline/MonadErrorTests.scala b/laws/src/main/scala/cats/laws/discipline/MonadErrorTests.scala index 5d74eeef45..4febbdb2a7 100644 --- a/laws/src/main/scala/cats/laws/discipline/MonadErrorTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/MonadErrorTests.scala @@ -42,7 +42,10 @@ trait MonadErrorTests[F[_], E] extends ApplicativeErrorTests[F, E] with MonadTes "monadError ensureOr consistency" -> forAll(laws.monadErrorEnsureOrConsistency[A] _), "monadError adaptError pure" -> forAll(laws.adaptErrorPure[A] _), "monadError adaptError raise" -> forAll(laws.adaptErrorRaise[A] _), - "monadError rethrow attempt" -> forAll(laws.rethrowAttempt[A] _) + "monadError rethrow attempt" -> forAll(laws.rethrowAttempt[A] _), + "monadError redeemWith is derived from attempt and flatMap" -> forAll( + laws.redeemWithDerivedFromAttemptFlatMap[A, B] _ + ) ) } } diff --git a/tests/src/test/scala/cats/tests/SyntaxSuite.scala b/tests/src/test/scala/cats/tests/SyntaxSuite.scala index f2d20fa70a..6d21b7afba 100644 --- a/tests/src/test/scala/cats/tests/SyntaxSuite.scala +++ b/tests/src/test/scala/cats/tests/SyntaxSuite.scala @@ -379,7 +379,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] @@ -402,12 +402,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]]]