Skip to content
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
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ val mimaSettings = Seq(
// Not a problem: IORunLoop is private
exclude[MissingClassProblem]("cats.effect.internals.IORunLoop$RestartCallback$"),
// Not a problem: Async.never implementation is just moved
exclude[ReversedMissingMethodProblem]("cats.effect.Async.never")
exclude[ReversedMissingMethodProblem]("cats.effect.Async.never"),
// Deleted
exclude[DirectMissingMethodProblem]("cats.effect.internals.IOFrame.errorHandler")
)
})

Expand Down
79 changes: 74 additions & 5 deletions core/shared/src/main/scala/cats/effect/IO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,74 @@ sealed abstract class IO[+A] extends internals.IOBinaryCompat[A] {
def bracketCase[B](use: A => IO[B])(release: (A, ExitCase[Throwable]) => IO[Unit]): IO[B] =
IOBracket(this)(use)(release)

override def toString = this match {
/**
* Handle any error, potentially recovering from it, by mapping it to another
* `IO` value.
*
* Implements `ApplicativeError.handleErrorWith`.
*/
def handleErrorWith[AA >: A](f: Throwable => IO[AA]): IO[AA] =
IO.Bind(this, new IOFrame.ErrorHandler(f))

/**
* 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 true:
*
* {{{
* io.redeem(recover, map) <-> io.attempt.map(_.fold(recover, map))
* }}}
*
* Usage of `redeem` subsumes `handleError` because:
*
* {{{
* io.redeem(fe, id) <-> io.handleError(fe)
* }}}
*
* @param recover is a function used for error recover in case the
* source ends in error
* @param map is a function used for mapping the result of the source
* in case it ends in success
*/
def redeem[B](recover: Throwable => B, map: A => B): IO[B] =
IO.Bind(this, new IOFrame.Redeem(recover, map))

/**
* 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:
*
* {{{
* io.redeemWith(recover, bind) <-> io.attempt.flatMap(_.fold(recover, bind))
* }}}
*
* Usage of `redeemWith` subsumes `handleErrorWith` because:
*
* {{{
* io.redeemWith(fe, F.pure) <-> io.handleErrorWith(fe)
* }}}
*
* Usage of `redeemWith` also subsumes [[flatMap]] because:
*
* {{{
* io.redeemWith(F.raiseError, fs) <-> io.flatMap(fs)
* }}}
*
* @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: Throwable => IO[B], bind: A => IO[B]): IO[B] =
IO.Bind(this, new IOFrame.RedeemWith(recover, bind))

override def toString: String = this match {
case Pure(a) => s"IO($a)"
case RaiseError(e) => s"IO(throw $e)"
case _ => "IO$" + System.identityHashCode(this)
Expand All @@ -546,7 +613,8 @@ sealed abstract class IO[+A] extends internals.IOBinaryCompat[A] {
private[effect] abstract class IOParallelNewtype
extends internals.IOTimerRef with internals.IOCompanionBinaryCompat {

/** Newtype encoding for an `IO` datatype that has a `cats.Applicative`
/**
* Newtype encoding for an `IO` datatype that has a `cats.Applicative`
* capable of doing parallel processing in `ap` and `map2`, needed
* for implementing `cats.Parallel`.
*
Expand All @@ -559,7 +627,8 @@ private[effect] abstract class IOParallelNewtype
*/
type Par[+A] = Par.Type[A]

/** Newtype encoding, see the [[IO.Par]] type alias
/**
* Newtype encoding, see the [[IO.Par]] type alias
* for more details.
*/
object Par extends IONewtype
Expand Down Expand Up @@ -608,7 +677,7 @@ private[effect] abstract class IOInstances extends IOLowPriorityInstances {
override def attempt[A](ioa: IO[A]): IO[Either[Throwable, A]] =
ioa.attempt
override def handleErrorWith[A](ioa: IO[A])(f: Throwable => IO[A]): IO[A] =
IO.Bind(ioa, IOFrame.errorHandler(f))
ioa.handleErrorWith(f)
override def raiseError[A](e: Throwable): IO[A] =
IO.raiseError(e)
override def suspend[A](thunk: => IO[A]): IO[A] =
Expand Down Expand Up @@ -637,7 +706,7 @@ private[effect] abstract class IOInstances extends IOLowPriorityInstances {
ioa
// this will use stack proportional to the maximum number of joined async suspensions
override def tailRecM[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] =
f(a) flatMap {
f(a).flatMap {
case Left(a) => tailRecM(a)(f)
case Right(b) => pure(b)
}
Expand Down
22 changes: 16 additions & 6 deletions core/shared/src/main/scala/cats/effect/internals/IOFrame.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ private[effect] abstract class IOFrame[-A, +R]
}

private[effect] object IOFrame {
/** Builds a [[IOFrame]] instance that maps errors, but that isn't
* defined for successful values (a partial function)
*/
def errorHandler[A](fe: Throwable => IO[A]): IOFrame[A, IO[A]] =
new ErrorHandler(fe)

/** [[IOFrame]] reference that only handles errors, useful for
* quick filtering of `onErrorHandleWith` frames.
*/
Expand All @@ -57,4 +51,20 @@ private[effect] object IOFrame {
def recover(e: Throwable): IO[A] = fe(e)
def apply(a: A): IO[A] = IO.pure(a)
}

/** Used by [[IO.redeem]]. */
final class Redeem[A, B](fe: Throwable => B, fs: A => B)
extends IOFrame[A, IO[B]] {

def apply(a: A): IO[B] = IO.pure(fs(a))
def recover(e: Throwable): IO[B] = IO.pure(fe(e))
}

/** Used by [[IO.redeemWith]]. */
final class RedeemWith[A, B](fe: Throwable => IO[B], fs: A => IO[B])
extends IOFrame[A, IO[B]] {

def apply(a: A): IO[B] = fs(a)
def recover(e: Throwable): IO[B] = fe(e)
}
}
24 changes: 24 additions & 0 deletions laws/shared/src/test/scala/cats/effect/IOTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,30 @@ class IOTests extends BaseTestsSuite {
p.future.value shouldBe None
}

testAsync("IO#redeem(throw, f) <-> IO#map") { implicit ec =>
check { (io: IO[Int], f: Int => Int) =>
io.redeem(e => throw e, f) <-> io.map(f)
}
}

testAsync("IO#redeem(f, identity) <-> IO#handleError") { implicit ec =>
check { (io: IO[Int], f: Throwable => Int) =>
io.redeem(f, identity) <-> io.handleError(f)
}
}

testAsync("IO#redeemWith(raiseError, f) <-> IO#flatMap") { implicit ec =>
check { (io: IO[Int], f: Int => IO[Int]) =>
io.redeemWith(IO.raiseError, f) <-> io.flatMap(f)
}
}

testAsync("IO#redeemWith(f, pure) <-> IO#handleErrorWith") { implicit ec =>
check { (io: IO[Int], f: Throwable => IO[Int]) =>
io.redeemWith(f, IO.pure) <-> io.handleErrorWith(f)
}
}

test("runSyncStep pure produces right IO") {
IO.pure(42).runSyncStep.unsafeRunSync() shouldBe Right(42)
}
Expand Down