Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add redeem and redeemWith #3146

Merged
merged 7 commits into from
Nov 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions core/src/main/scala/cats/ApplicativeError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
38 changes: 38 additions & 0 deletions core/src/main/scala/cats/MonadError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/instances/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
53 changes: 33 additions & 20 deletions core/src/main/scala/cats/instances/future.scala
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
package cats
package instances

import scala.util.control.NonFatal
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

trait FutureInstances extends FutureInstances1 {

implicit def catsStdInstancesForFuture(
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)
}
}

Expand Down
14 changes: 14 additions & 0 deletions core/src/main/scala/cats/instances/option.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion core/src/main/scala/cats/instances/try.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/syntax/applicativeError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/syntax/monadError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions laws/src/main/scala/cats/laws/ApplicativeErrorLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions laws/src/main/scala/cats/laws/MonadErrorLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] _)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] _
)
)
}
}
Expand Down
39 changes: 38 additions & 1 deletion tests/src/test/scala/cats/tests/SyntaxSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]]]

Expand Down