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

Added orElse combinator to IO #2940

Merged
merged 5 commits into from
Apr 6, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 9 additions & 0 deletions core/shared/src/main/scala/cats/effect/IO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,15 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] {
def handleError[B >: A](f: Throwable => B): IO[B] =
handleErrorWith[B](t => IO.pure(f(t)))

/**
* Runs the current IO, if it fails with an error(exception), the other IO will be executed.
* @param other
* IO to be executed (if the current IO fails)
* @return
*/
def orElse[B >: A](other: IO[B]): IO[B] =
handleErrorWith(_ => other)
Copy link
Contributor

Choose a reason for hiding this comment

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

If it just calls to handleErrorWith then (perhaps) it would be better to add such a method directly to Cats' ApplicativeError type class instead of just IO. Because apparently it would make orElse functionality uniformly available for all ApplicativeError implementers (which includes IO from Cats Effect 2.x, Monix Task, fs2.Stream, etc). Wdyt?

Copy link
Member

Choose a reason for hiding this comment

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

It's already syntax on ApplicativeError. I'm not sure why it shouldn't also be on the type class.

Regardless, I think the trend on IO has been toward a rich interface without typeclass syntax. Adding it here is consistent with Either, EitherT, etc.

Copy link
Contributor

@satorg satorg Apr 5, 2022

Choose a reason for hiding this comment

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

I'm not sure why it shouldn't also be on the type class.

I'm not sure either. I wonder if there is any convention on that in general. The syntax was introduced in typelevel/cats#2243 but seems the option for adding it to the typeclass had not been discussed nor challenged.

In general, having a method on typeclass makes it possible to override it in a particular implementation (e.g. for the sake of optimization). Not sure if it may make sense to do that for this orElse though.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's already syntax on ApplicativeError.

Btw, the syntax for ApplicativeError is defined in this way:

  def orElse(other: => F[A])(implicit F: ApplicativeError[F, E]): F[A] =
    F.handleErrorWith(fa)(_ => other)

Notice it takes other as a by-name param while the suggested addition to IO takes it as just a value.

Wouldn't it be better to keep these two methods in correspondence to each other? I.e. to declare the latter's other as a by-name parameter too? @rossabaker @djspiewak , wdyt?

Copy link
Member

Choose a reason for hiding this comment

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

Right, good catch. This should definitely be a by-name.

Copy link
Member

Choose a reason for hiding this comment

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

Btw, it's really OOPy but if we had inheritable syntax traits for the monad classes themselves, couldn't we get all this syntax for free and avoid oopsies? Similar to how I've suggested that the IO companion object itself should implement Async[IO].

Copy link
Member

Choose a reason for hiding this comment

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

So the by-name parameter point is a really important one. If this already exists as syntax on ApplicativeError, then we need to be consistent with that.


/**
* Handle any error, potentially recovering from it, by mapping it to another `IO` value.
*
Expand Down
10 changes: 10 additions & 0 deletions tests/shared/src/test/scala/cats/effect/IOSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification {
IO.raiseError[Unit](TestException).attempt must completeAs(Left(TestException))
}

"orElse must return other if previous IO Fails" in ticked { implicit ticker =>
case object TestException extends RuntimeException
(IO.raiseError[Int](TestException) orElse IO.pure(42)) must completeAs(42)
}

"Return current IO if successful" in ticked { implicit ticker =>
case object TestException extends RuntimeException
(IO.pure(42) orElse IO.raiseError[Int](TestException)) must completeAs(42)
}

"attempt is redeem with Left(_) for recover and Right(_) for map" in ticked {
implicit ticker =>
forAll { (io: IO[Int]) => io.attempt eqv io.redeem(Left(_), Right(_)) }
Expand Down