Skip to content

Commit 3457276

Browse files
committed
Enhance EitherT and fix typelevel#2161: add MonadError.redeem/redeemWith
1 parent ef05c76 commit 3457276

File tree

18 files changed

+1275
-146
lines changed

18 files changed

+1275
-146
lines changed

build.sbt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ lazy val commonSettings = Seq(
4949
scalacOptions in (Compile, doc) := (scalacOptions in (Compile, doc)).value.filter(_ != "-Xfatal-warnings"),
5050
ivyConfigurations += CompileTime,
5151
unmanagedClasspath in Compile ++= update.value.select(configurationFilter(CompileTime.name)),
52+
unmanagedSourceDirectories in Compile ++= {
53+
val bd = baseDirectory.value
54+
if (CrossVersion.partialVersion(scalaVersion.value) exists (_._2 >= 12))
55+
CrossType.Pure.sharedSrcDir(bd, "main").toList map (f => file(f.getPath + "-2.12+"))
56+
else
57+
CrossType.Pure.sharedSrcDir(bd, "main").toList map (f => file(f.getPath + "-2.11-"))
58+
},
5259
unmanagedSourceDirectories in Test ++= {
5360
val bd = baseDirectory.value
5461
if (CrossVersion.partialVersion(scalaVersion.value) exists (_._2 >= 11))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package cats.internals
2+
3+
import scala.concurrent.{ExecutionContext, Future}
4+
5+
private[cats] object FutureShims {
6+
/**
7+
* Exposes the new `Future#transformWith` from Scala 2.12 in a way
8+
* that's compatible with Scala 2.11.
9+
*/
10+
def redeemWith[A, B](fa: Future[A])(fe: Throwable => Future[B], fs: A => Future[B])
11+
(implicit ec: ExecutionContext): Future[B] =
12+
attempt(fa).flatMap(_.fold(fe, fs))
13+
14+
/**
15+
* Exposes an optimized `attempt` for `Future` whose implementation
16+
* depends on the Scala version.
17+
*/
18+
def attempt[A](fa: Future[A])(implicit ec: ExecutionContext): Future[Either[Throwable, A]] =
19+
fa.map(Right[Throwable, A]).recover { case e => Left(e) }
20+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package cats.internals
2+
3+
import scala.concurrent.{ExecutionContext, Future}
4+
import scala.util.{Failure, Success}
5+
6+
private[cats] object FutureShims {
7+
/**
8+
* Exposes the new `Future#transformWith` from Scala 2.12 in a way
9+
* that's compatible with Scala 2.11.
10+
*/
11+
def redeemWith[A, B](fa: Future[A])(fe: Throwable => Future[B], fs: A => Future[B])
12+
(implicit ec: ExecutionContext): Future[B] = {
13+
14+
fa.transformWith {
15+
case Success(a) => fs(a)
16+
case Failure(e) => fe(e)
17+
}
18+
}
19+
20+
/**
21+
* Exposes an optimized `attempt` for `Future` whose implementation
22+
* depends on the Scala version.
23+
*/
24+
def attempt[A](fa: Future[A])(implicit ec: ExecutionContext): Future[Either[Throwable, A]] =
25+
fa.transformWith(r => Future.successful(
26+
r match {
27+
case Success(a) => Right(a)
28+
case Failure(e) => Left(e)
29+
}))
30+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cats
33
import cats.data.EitherT
44
import scala.util.{ Failure, Success, Try }
55
import scala.util.control.NonFatal
6+
import cats.internals.EitherUtil.rightBox
67

78
/**
89
* An applicative that also allows you to raise and or handle an error value.
@@ -62,9 +63,8 @@ trait ApplicativeError[F[_], E] extends Applicative[F] {
6263
*
6364
* All non-fatal errors should be handled by this method.
6465
*/
65-
def attempt[A](fa: F[A]): F[Either[E, A]] = handleErrorWith(
66-
map(fa)(Right(_): Either[E, A])
67-
)(e => pure(Left(e)))
66+
def attempt[A](fa: F[A]): F[Either[E, A]] =
67+
handleErrorWith(map(fa)(rightBox[E, A]))(e => pure(Left[E, A](e)))
6868

6969
/**
7070
* Similar to [[attempt]], but wraps the result in a [[cats.data.EitherT]] for

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,76 @@ trait MonadError[F[_], E] extends ApplicativeError[F, E] with Monad[F] {
6161
*/
6262
def rethrow[A](fa: F[Either[E, A]]): F[A] =
6363
flatMap(fa)(_.fold(raiseError, pure))
64+
65+
/**
66+
* Returns a new value that transforms the result of the source,
67+
* given the `recover` or `map` functions, which get executed depending
68+
* on whether the result is successful or if it ends in error.
69+
*
70+
* This is an optimization on usage of [[attempt]] and [[map]],
71+
* this equivalence being available:
72+
*
73+
* {{{
74+
* fa.redeem(fe, fs) <-> fa.attempt.map(_.fold(fe, fs))
75+
* }}}
76+
*
77+
* Usage of `redeem` subsumes [[handleError]] because:
78+
*
79+
* {{{
80+
* fa.redeem(fe, id) <-> fa.handleError(fe)
81+
* }}}
82+
*
83+
* Implementations are free to override it in order to optimize
84+
* error recovery.
85+
*
86+
* @see [[redeemWith]], [[attempt]] and [[handleError]]
87+
*
88+
* @param fa is the source whose result is going to get transformed
89+
* @param recover is the function that gets called to recover the source
90+
* in case of error
91+
* @param map is the function that gets to transform the source
92+
* in case of success
93+
*/
94+
def redeem[A, B](fa: F[A])(recover: E => B, map: A => B): F[B] =
95+
redeemWith(fa)(recover.andThen(pure), map.andThen(pure))
96+
97+
/**
98+
* Returns a new value that transforms the result of the source,
99+
* given the `recover` or `bind` functions, which get executed depending
100+
* on whether the result is successful or if it ends in error.
101+
*
102+
* This is an optimization on usage of [[attempt]] and [[flatMap]],
103+
* this equivalence being available:
104+
*
105+
* {{{
106+
* fa.redeemWith(fe, fs) <-> fa.attempt.flatMap(_.fold(fe, fs))
107+
* }}}
108+
*
109+
* Usage of `redeemWith` subsumes [[handleErrorWith]] because:
110+
*
111+
* {{{
112+
* fa.redeemWith(fe, F.pure) <-> fa.handleErrorWith(fe)
113+
* }}}
114+
*
115+
* Usage of `redeemWith` also subsumes [[flatMap]] because:
116+
*
117+
* {{{
118+
* fa.redeemWith(F.raiseError, fs) <-> fa.flatMap(fs)
119+
* }}}
120+
*
121+
* Implementations are free to override it in order to optimize
122+
* error recovery.
123+
*
124+
* @see [[redeem]], [[attempt]] and [[handleErrorWith]]
125+
*
126+
* @param fa is the source whose result is going to get transformed
127+
* @param recover is the function that gets called to recover the source
128+
* in case of error
129+
* @param bind is the function that gets to transform the source
130+
* in case of success
131+
*/
132+
def redeemWith[A, B](fa: F[A])(recover: E => F[B], bind: A => F[B]): F[B] =
133+
flatMap(attempt(fa))(_.fold(recover, bind))
64134
}
65135

66136
object MonadError {

core/src/main/scala/cats/Parallel.scala

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,30 +78,32 @@ trait Parallel[M[_], F[_]] extends NonEmptyParallel[M, F] {
7878
*/
7979
def applicativeError[E](implicit E: MonadError[M, E]): ApplicativeError[F, E] = new ApplicativeError[F, E] {
8080

81-
def raiseError[A](e: E): F[A] =
81+
override def raiseError[A](e: E): F[A] =
8282
parallel(MonadError[M, E].raiseError(e))
8383

84-
def handleErrorWith[A](fa: F[A])(f: (E) => F[A]): F[A] = {
84+
override def handleErrorWith[A](fa: F[A])(f: (E) => F[A]): F[A] = {
8585
val ma = MonadError[M, E].handleErrorWith(sequential(fa))(f andThen sequential.apply)
8686
parallel(ma)
8787
}
8888

89-
def pure[A](x: A): F[A] = applicative.pure(x)
90-
91-
def ap[A, B](ff: F[(A) => B])(fa: F[A]): F[B] = applicative.ap(ff)(fa)
92-
93-
override def map[A, B](fa: F[A])(f: (A) => B): F[B] = applicative.map(fa)(f)
94-
95-
override def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] = applicative.product(fa, fb)
96-
97-
override def map2[A, B, Z](fa: F[A], fb: F[B])(f: (A, B) => Z): F[Z] = applicative.map2(fa, fb)(f)
98-
89+
override def pure[A](x: A): F[A] =
90+
applicative.pure(x)
91+
override def ap[A, B](ff: F[(A) => B])(fa: F[A]): F[B] =
92+
applicative.ap(ff)(fa)
93+
override def attempt[A](fa: F[A]): F[Either[E, A]] =
94+
parallel(MonadError[M, E].attempt(sequential(fa)))
95+
override def map[A, B](fa: F[A])(f: (A) => B): F[B] =
96+
applicative.map(fa)(f)
97+
override def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] =
98+
applicative.product(fa, fb)
99+
override def map2[A, B, Z](fa: F[A], fb: F[B])(f: (A, B) => Z): F[Z] =
100+
applicative.map2(fa, fb)(f)
99101
override def map2Eval[A, B, Z](fa: F[A], fb: Eval[F[B]])(f: (A, B) => Z): Eval[F[Z]] =
100102
applicative.map2Eval(fa, fb)(f)
101-
102-
override def unlessA[A](cond: Boolean)(f: => F[A]): F[Unit] = applicative.unlessA(cond)(f)
103-
104-
override def whenA[A](cond: Boolean)(f: => F[A]): F[Unit] = applicative.whenA(cond)(f)
103+
override def unlessA[A](cond: Boolean)(f: => F[A]): F[Unit] =
104+
applicative.unlessA(cond)(f)
105+
override def whenA[A](cond: Boolean)(f: => F[A]): F[Unit] =
106+
applicative.whenA(cond)(f)
105107
}
106108
}
107109

0 commit comments

Comments
 (0)