Skip to content

Commit 53a0f59

Browse files
authored
Merge pull request #3146 from travisbrown/topic/add-redeem
Add redeem and redeemWith
2 parents 4af62d7 + 6ba59df commit 53a0f59

File tree

13 files changed

+189
-24
lines changed

13 files changed

+189
-24
lines changed

core/src/main/scala/cats/ApplicativeError.scala

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,38 @@ trait ApplicativeError[F[_], E] extends Applicative[F] {
103103
def recoverWith[A](fa: F[A])(pf: PartialFunction[E, F[A]]): F[A] =
104104
handleErrorWith(fa)(e => pf.applyOrElse(e, raiseError))
105105

106+
/**
107+
* Returns a new value that transforms the result of the source,
108+
* given the `recover` or `map` functions, which get executed depending
109+
* on whether the result is successful or if it ends in error.
110+
*
111+
* This is an optimization on usage of [[attempt]] and [[map]],
112+
* this equivalence being available:
113+
*
114+
* {{{
115+
* fa.redeem(fe, fs) <-> fa.attempt.map(_.fold(fe, fs))
116+
* }}}
117+
*
118+
* Usage of `redeem` subsumes [[handleError]] because:
119+
*
120+
* {{{
121+
* fa.redeem(fe, id) <-> fa.handleError(fe)
122+
* }}}
123+
*
124+
* Implementations are free to override it in order to optimize
125+
* error recovery.
126+
*
127+
* @see [[MonadError.redeemWith]], [[attempt]] and [[handleError]]
128+
*
129+
* @param fa is the source whose result is going to get transformed
130+
* @param recover is the function that gets called to recover the source
131+
* in case of error
132+
* @param map is the function that gets to transform the source
133+
* in case of success
134+
*/
135+
def redeem[A, B](fa: F[A])(recover: E => B, f: A => B): F[B] =
136+
handleError(map(fa)(f))(recover)
137+
106138
/**
107139
* Execute a callback on certain errors, then rethrow them.
108140
* Any non matching error is rethrown as well.

core/src/main/scala/cats/MonadError.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,44 @@ trait MonadError[F[_], E] extends ApplicativeError[F, E] with Monad[F] {
6565
*/
6666
def rethrow[A, EE <: E](fa: F[Either[EE, A]]): F[A] =
6767
flatMap(fa)(_.fold(raiseError, pure))
68+
69+
/**
70+
* Returns a new value that transforms the result of the source,
71+
* given the `recover` or `bind` functions, which get executed depending
72+
* on whether the result is successful or if it ends in error.
73+
*
74+
* This is an optimization on usage of [[attempt]] and [[flatMap]],
75+
* this equivalence being available:
76+
*
77+
* {{{
78+
* fa.redeemWith(fe, fs) <-> fa.attempt.flatMap(_.fold(fe, fs))
79+
* }}}
80+
*
81+
* Usage of `redeemWith` subsumes [[handleErrorWith]] because:
82+
*
83+
* {{{
84+
* fa.redeemWith(fe, F.pure) <-> fa.handleErrorWith(fe)
85+
* }}}
86+
*
87+
* Usage of `redeemWith` also subsumes [[flatMap]] because:
88+
*
89+
* {{{
90+
* fa.redeemWith(F.raiseError, fs) <-> fa.flatMap(fs)
91+
* }}}
92+
*
93+
* Implementations are free to override it in order to optimize
94+
* error recovery.
95+
*
96+
* @see [[redeem]], [[attempt]] and [[handleErrorWith]]
97+
*
98+
* @param fa is the source whose result is going to get transformed
99+
* @param recover is the function that gets called to recover the source
100+
* in case of error
101+
* @param bind is the function that gets to transform the source
102+
* in case of success
103+
*/
104+
def redeemWith[A, B](fa: F[A])(recover: E => F[B], bind: A => F[B]): F[B] =
105+
flatMap(attempt(fa))(_.fold(recover, bind))
68106
}
69107

70108
object MonadError {

core/src/main/scala/cats/instances/either.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ trait EitherInstances extends cats.kernel.instances.EitherInstances {
9898
override def recoverWith[B](fab: Either[A, B])(pf: PartialFunction[A, Either[A, B]]): Either[A, B] =
9999
fab.recoverWith(pf)
100100

101+
override def redeem[B, R](fab: Either[A, B])(recover: A => R, map: B => R): Either[A, R] =
102+
Right(fab.fold(recover, map))
103+
104+
override def redeemWith[B, R](fab: Either[A, B])(recover: A => Either[A, R],
105+
bind: B => Either[A, R]): Either[A, R] =
106+
fab.fold(recover, bind)
107+
101108
override def fromEither[B](fab: Either[A, B]): Either[A, B] =
102109
fab
103110

core/src/main/scala/cats/instances/future.scala

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,50 @@
11
package cats
22
package instances
33

4-
import scala.util.control.NonFatal
54
import scala.concurrent.{ExecutionContext, Future}
5+
import scala.util.{Failure, Success}
66

77
trait FutureInstances extends FutureInstances1 {
88

99
implicit def catsStdInstancesForFuture(
1010
implicit ec: ExecutionContext
1111
): MonadError[Future, Throwable] with CoflatMap[Future] with Monad[Future] =
1212
new FutureCoflatMap with MonadError[Future, Throwable] with Monad[Future] with StackSafeMonad[Future] {
13-
def pure[A](x: A): Future[A] = Future.successful(x)
14-
15-
def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = fa.flatMap(f)
16-
17-
def handleErrorWith[A](fea: Future[A])(f: Throwable => Future[A]): Future[A] = fea.recoverWith { case t => f(t) }
18-
19-
def raiseError[A](e: Throwable): Future[A] = Future.failed(e)
20-
override def handleError[A](fea: Future[A])(f: Throwable => A): Future[A] = fea.recover { case t => f(t) }
21-
13+
override def pure[A](x: A): Future[A] =
14+
Future.successful(x)
15+
override def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] =
16+
fa.flatMap(f)
17+
override def handleErrorWith[A](fea: Future[A])(f: Throwable => Future[A]): Future[A] =
18+
fea.recoverWith { case t => f(t) }
19+
override def raiseError[A](e: Throwable): Future[A] =
20+
Future.failed(e)
21+
override def handleError[A](fea: Future[A])(f: Throwable => A): Future[A] =
22+
fea.recover { case t => f(t) }
2223
override def attempt[A](fa: Future[A]): Future[Either[Throwable, A]] =
23-
(fa.map(a => Right[Throwable, A](a))).recover { case NonFatal(t) => Left(t) }
24-
25-
override def recover[A](fa: Future[A])(pf: PartialFunction[Throwable, A]): Future[A] = fa.recover(pf)
26-
24+
fa.transformWith(
25+
r =>
26+
Future.successful(
27+
r match {
28+
case Success(a) => Right(a)
29+
case Failure(e) => Left(e)
30+
}
31+
)
32+
)
33+
override def redeemWith[A, B](fa: Future[A])(recover: Throwable => Future[B], bind: A => Future[B]): Future[B] =
34+
fa.transformWith {
35+
case Success(a) => bind(a)
36+
case Failure(e) => recover(e)
37+
}
38+
override def recover[A](fa: Future[A])(pf: PartialFunction[Throwable, A]): Future[A] =
39+
fa.recover(pf)
2740
override def recoverWith[A](fa: Future[A])(pf: PartialFunction[Throwable, Future[A]]): Future[A] =
2841
fa.recoverWith(pf)
29-
30-
override def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f)
31-
32-
override def catchNonFatal[A](a: => A)(implicit ev: Throwable <:< Throwable): Future[A] = Future(a)
33-
34-
override def catchNonFatalEval[A](a: Eval[A])(implicit ev: Throwable <:< Throwable): Future[A] = Future(a.value)
42+
override def map[A, B](fa: Future[A])(f: A => B): Future[B] =
43+
fa.map(f)
44+
override def catchNonFatal[A](a: => A)(implicit ev: Throwable <:< Throwable): Future[A] =
45+
Future(a)
46+
override def catchNonFatalEval[A](a: Eval[A])(implicit ev: Throwable <:< Throwable): Future[A] =
47+
Future(a.value)
3548
}
3649
}
3750

core/src/main/scala/cats/instances/option.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ trait OptionInstances extends cats.kernel.instances.OptionInstances {
6767

6868
def handleErrorWith[A](fa: Option[A])(f: (Unit) => Option[A]): Option[A] = fa.orElse(f(()))
6969

70+
override def redeem[A, B](fa: Option[A])(recover: Unit => B, map: A => B): Option[B] =
71+
fa match {
72+
case Some(a) => Some(map(a))
73+
// N.B. not pattern matching `case None` on purpose
74+
case _ => Some(recover(()))
75+
}
76+
77+
override def redeemWith[A, B](fa: Option[A])(recover: Unit => Option[B], bind: A => Option[B]): Option[B] =
78+
fa match {
79+
case Some(a) => bind(a)
80+
// N.B. not pattern matching `case None` on purpose
81+
case _ => recover(())
82+
}
83+
7084
def traverse[G[_]: Applicative, A, B](fa: Option[A])(f: A => G[B]): G[Option[B]] =
7185
fa match {
7286
case None => Applicative[G].pure(None)

core/src/main/scala/cats/instances/try.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,15 @@ trait TryInstances extends TryInstances1 {
6969
ta.recover { case t => f(t) }
7070

7171
override def attempt[A](ta: Try[A]): Try[Either[Throwable, A]] =
72-
(ta.map(a => Right[Throwable, A](a))).recover { case NonFatal(t) => Left(t) }
72+
ta match { case Success(a) => Success(Right(a)); case Failure(e) => Success(Left(e)) }
73+
74+
override def redeem[A, B](ta: Try[A])(recover: Throwable => B, map: A => B): Try[B] =
75+
ta match { case Success(a) => Try(map(a)); case Failure(e) => Try(recover(e)) }
76+
77+
override def redeemWith[A, B](ta: Try[A])(recover: Throwable => Try[B], bind: A => Try[B]): Try[B] =
78+
try ta match {
79+
case Success(a) => bind(a); case Failure(e) => recover(e)
80+
} catch { case NonFatal(e) => Failure(e) }
7381

7482
override def recover[A](ta: Try[A])(pf: PartialFunction[Throwable, A]): Try[A] =
7583
ta.recover(pf)

core/src/main/scala/cats/syntax/applicativeError.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ final class ApplicativeErrorOps[F[_], E, A](private val fa: F[A]) extends AnyVal
9797
def recoverWith(pf: PartialFunction[E, F[A]])(implicit F: ApplicativeError[F, E]): F[A] =
9898
F.recoverWith(fa)(pf)
9999

100+
def redeem[B](recover: E => B, f: A => B)(implicit F: ApplicativeError[F, E]): F[B] =
101+
F.redeem[A, B](fa)(recover, f)
102+
100103
def onError(pf: PartialFunction[E, F[Unit]])(implicit F: ApplicativeError[F, E]): F[A] =
101104
F.onError(fa)(pf)
102105

core/src/main/scala/cats/syntax/monadError.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ final class MonadErrorOps[F[_], E, A](private val fa: F[A]) extends AnyVal {
2929

3030
def adaptError(pf: PartialFunction[E, E])(implicit F: MonadError[F, E]): F[A] =
3131
F.adaptError(fa)(pf)
32+
33+
def redeemWith[B](recover: E => F[B], bind: A => F[B])(implicit F: MonadError[F, E]): F[B] =
34+
F.redeemWith[A, B](fa)(recover, bind)
3235
}
3336

3437
final class MonadErrorRethrowOps[F[_], E, A](private val fea: F[Either[E, A]]) extends AnyVal {

laws/src/main/scala/cats/laws/ApplicativeErrorLaws.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ trait ApplicativeErrorLaws[F[_], E] extends ApplicativeLaws[F] {
4545

4646
def onErrorRaise[A](fa: F[A], e: E, fb: F[Unit]): IsEq[F[A]] =
4747
F.onError(F.raiseError[A](e)) { case err => fb } <-> F.map2(fb, F.raiseError[A](e))((_, b) => b)
48+
49+
def redeemDerivedFromAttemptMap[A, B](fa: F[A], fe: E => B, fs: A => B): IsEq[F[B]] =
50+
F.redeem(fa)(fe, fs) <-> F.map(F.attempt(fa))(_.fold(fe, fs))
4851
}
4952

5053
object ApplicativeErrorLaws {

laws/src/main/scala/cats/laws/MonadErrorLaws.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ trait MonadErrorLaws[F[_], E] extends ApplicativeErrorLaws[F, E] with MonadLaws[
2222

2323
def rethrowAttempt[A](fa: F[A]): IsEq[F[A]] =
2424
F.rethrow(F.attempt(fa)) <-> fa
25+
26+
def redeemWithDerivedFromAttemptFlatMap[A, B](fa: F[A], fe: E => F[B], fs: A => F[B]): IsEq[F[B]] =
27+
F.redeemWith(fa)(fe, fs) <-> F.flatMap(F.attempt(fa))(_.fold(fe, fs))
2528
}
2629

2730
object MonadErrorLaws {

0 commit comments

Comments
 (0)