Skip to content

Commit

Permalink
Enhance EitherT and fix typelevel#2161: add MonadError.redeem/redeemWith
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandru committed Apr 23, 2018
1 parent ef05c76 commit 3457276
Show file tree
Hide file tree
Showing 18 changed files with 1,275 additions and 146 deletions.
7 changes: 7 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ lazy val commonSettings = Seq(
scalacOptions in (Compile, doc) := (scalacOptions in (Compile, doc)).value.filter(_ != "-Xfatal-warnings"),
ivyConfigurations += CompileTime,
unmanagedClasspath in Compile ++= update.value.select(configurationFilter(CompileTime.name)),
unmanagedSourceDirectories in Compile ++= {
val bd = baseDirectory.value
if (CrossVersion.partialVersion(scalaVersion.value) exists (_._2 >= 12))
CrossType.Pure.sharedSrcDir(bd, "main").toList map (f => file(f.getPath + "-2.12+"))
else
CrossType.Pure.sharedSrcDir(bd, "main").toList map (f => file(f.getPath + "-2.11-"))
},
unmanagedSourceDirectories in Test ++= {
val bd = baseDirectory.value
if (CrossVersion.partialVersion(scalaVersion.value) exists (_._2 >= 11))
Expand Down
20 changes: 20 additions & 0 deletions core/src/main/scala-2.11-/cats/internals/FutureShims.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package cats.internals

import scala.concurrent.{ExecutionContext, Future}

private[cats] object FutureShims {
/**
* Exposes the new `Future#transformWith` from Scala 2.12 in a way
* that's compatible with Scala 2.11.
*/
def redeemWith[A, B](fa: Future[A])(fe: Throwable => Future[B], fs: A => Future[B])
(implicit ec: ExecutionContext): Future[B] =
attempt(fa).flatMap(_.fold(fe, fs))

/**
* Exposes an optimized `attempt` for `Future` whose implementation
* depends on the Scala version.
*/
def attempt[A](fa: Future[A])(implicit ec: ExecutionContext): Future[Either[Throwable, A]] =
fa.map(Right[Throwable, A]).recover { case e => Left(e) }
}
30 changes: 30 additions & 0 deletions core/src/main/scala-2.12+/cats/internals/FutureShims.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cats.internals

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

private[cats] object FutureShims {
/**
* Exposes the new `Future#transformWith` from Scala 2.12 in a way
* that's compatible with Scala 2.11.
*/
def redeemWith[A, B](fa: Future[A])(fe: Throwable => Future[B], fs: A => Future[B])
(implicit ec: ExecutionContext): Future[B] = {

fa.transformWith {
case Success(a) => fs(a)
case Failure(e) => fe(e)
}
}

/**
* Exposes an optimized `attempt` for `Future` whose implementation
* depends on the Scala version.
*/
def attempt[A](fa: Future[A])(implicit ec: ExecutionContext): Future[Either[Throwable, A]] =
fa.transformWith(r => Future.successful(
r match {
case Success(a) => Right(a)
case Failure(e) => Left(e)
}))
}
6 changes: 3 additions & 3 deletions core/src/main/scala/cats/ApplicativeError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cats
import cats.data.EitherT
import scala.util.{ Failure, Success, Try }
import scala.util.control.NonFatal
import cats.internals.EitherUtil.rightBox

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

/**
* Similar to [[attempt]], but wraps the result in a [[cats.data.EitherT]] for
Expand Down
70 changes: 70 additions & 0 deletions core/src/main/scala/cats/MonadError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,76 @@ trait MonadError[F[_], E] extends ApplicativeError[F, E] with Monad[F] {
*/
def rethrow[A](fa: F[Either[E, A]]): F[A] =
flatMap(fa)(_.fold(raiseError, pure))

/**
* 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 [[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, map: A => B): F[B] =
redeemWith(fa)(recover.andThen(pure), map.andThen(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
34 changes: 18 additions & 16 deletions core/src/main/scala/cats/Parallel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,30 +78,32 @@ trait Parallel[M[_], F[_]] extends NonEmptyParallel[M, F] {
*/
def applicativeError[E](implicit E: MonadError[M, E]): ApplicativeError[F, E] = new ApplicativeError[F, E] {

def raiseError[A](e: E): F[A] =
override def raiseError[A](e: E): F[A] =
parallel(MonadError[M, E].raiseError(e))

def handleErrorWith[A](fa: F[A])(f: (E) => F[A]): F[A] = {
override def handleErrorWith[A](fa: F[A])(f: (E) => F[A]): F[A] = {
val ma = MonadError[M, E].handleErrorWith(sequential(fa))(f andThen sequential.apply)
parallel(ma)
}

def pure[A](x: A): F[A] = applicative.pure(x)

def ap[A, B](ff: F[(A) => B])(fa: F[A]): F[B] = applicative.ap(ff)(fa)

override def map[A, B](fa: F[A])(f: (A) => B): F[B] = applicative.map(fa)(f)

override def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] = applicative.product(fa, fb)

override def map2[A, B, Z](fa: F[A], fb: F[B])(f: (A, B) => Z): F[Z] = applicative.map2(fa, fb)(f)

override def pure[A](x: A): F[A] =
applicative.pure(x)
override def ap[A, B](ff: F[(A) => B])(fa: F[A]): F[B] =
applicative.ap(ff)(fa)
override def attempt[A](fa: F[A]): F[Either[E, A]] =
parallel(MonadError[M, E].attempt(sequential(fa)))
override def map[A, B](fa: F[A])(f: (A) => B): F[B] =
applicative.map(fa)(f)
override def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] =
applicative.product(fa, fb)
override def map2[A, B, Z](fa: F[A], fb: F[B])(f: (A, B) => Z): F[Z] =
applicative.map2(fa, fb)(f)
override def map2Eval[A, B, Z](fa: F[A], fb: Eval[F[B]])(f: (A, B) => Z): Eval[F[Z]] =
applicative.map2Eval(fa, fb)(f)

override def unlessA[A](cond: Boolean)(f: => F[A]): F[Unit] = applicative.unlessA(cond)(f)

override def whenA[A](cond: Boolean)(f: => F[A]): F[Unit] = applicative.whenA(cond)(f)
override def unlessA[A](cond: Boolean)(f: => F[A]): F[Unit] =
applicative.unlessA(cond)(f)
override def whenA[A](cond: Boolean)(f: => F[A]): F[Unit] =
applicative.whenA(cond)(f)
}
}

Expand Down
Loading

0 comments on commit 3457276

Please sign in to comment.