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

Backport #3146 Add redeem and redeemWith #3348

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
30 changes: 20 additions & 10 deletions core/src/main/scala/cats/instances/future.scala
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
package cats
package instances

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

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 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] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no actual changes in this file, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, only formatting

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)
}
}

Expand Down
7 changes: 3 additions & 4 deletions core/src/main/scala/cats/instances/try.scala
Original file line number Diff line number Diff line change
@@ -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 {

Expand Down Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions core/src/main/scala/cats/syntax/applicativeError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
34 changes: 34 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,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 {
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 @@ -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]
Expand All @@ -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]]]

Expand Down