An interesting thing about Cats Effect is that it is as much a library providing typeclasses for functional effect as much as it is an implementation of Ref
, IO
and other datatypes.
MonadCancel
is a fundamental typeclass extending MonadError
(Monad extension of ApplicativeError
) supporting safe cancelation, masking, and finalization. We can think of this as functional construct for try-catch-finally.
trait MonadCancel[F[_], E] extends MonadError[F, E] {
def rootCancelScope: CancelScope
def forceR[A, B](fa: F[A])(fb: F[B]): F[B]
def uncancelable[A](body: Poll[F] => F[A]): F[A]
def canceled: F[Unit]
def onCancel[A](fa: F[A], fin: F[Unit]): F[A]
def bracket[A, B](acquire: F[A])(use: A => F[B])(release: A => F[Unit]): F[B] =
bracketCase(acquire)(use)((a, _) => release(a))
def bracketCase[A, B](acquire: F[A])(use: A => F[B])(
release: (A, Outcome[F, E, B]) => F[Unit]): F[B] =
bracketFull(_ => acquire)(use)(release)
def bracketFull[A, B](acquire: Poll[F] => F[A])(use: A => F[B])(
release: (A, Outcome[F, E, B]) => F[Unit]): F[B]
}
MonadCancel documentation says:
One particularly unique aspect of
MonadCancel
is the ability to self-cancel.
import cats._, cats.syntax.all._
import cats.effect.IO
lazy val program = IO.canceled >> IO.println("nope")
scala> {
import cats.effect.unsafe.implicits.global
program.unsafeRunSync()
}
java.util.concurrent.CancellationException: Main fiber was canceled
at cats.effect.IO.$anonfun$unsafeRunAsync$1(IO.scala:640)
at cats.effect.IO.$anonfun$unsafeRunFiber$2(IO.scala:702)
at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
at cats.effect.kernel.Outcome.fold(Outcome.scala:37)
at cats.effect.kernel.Outcome.fold$(Outcome.scala:35)
at cats.effect.kernel.Outcome$Canceled.fold(Outcome.scala:181)
at cats.effect.IO.$anonfun$unsafeRunFiber$1(IO.scala:708)
at cats.effect.IO.$anonfun$unsafeRunFiber$1$adapted(IO.scala:698)
at cats.effect.CallbackStack.apply(CallbackStack.scala:45)
at cats.effect.IOFiber.done(IOFiber.scala:894)
at cats.effect.IOFiber.asyncCancel(IOFiber.scala:941)
at cats.effect.IOFiber.runLoop(IOFiber.scala:458)
at cats.effect.IOFiber.execR(IOFiber.scala:1117)
at cats.effect.IOFiber.run(IOFiber.scala:125)
at cats.effect.unsafe.WorkerThread.run(WorkerThread.scala:358)
Here’s a less dramatic one:
{
import cats.effect.unsafe.implicits.global
program.unsafeRunAndForget()
}
Either case, the effect is canceled, and "nope"
action does not take place.
Note that the idea of cancellation is also scripted inside of the IO datatype. This is contrasting to Monix Task
where the cancellation happens against CancelableFuture
, after the end of the world so to speak.
Since cancellation can be inconvenient at certain timings, MonadCancel
actually provides a uncancelable
region, which can be used like this:
lazy val program2 = IO.uncancelable { _ =>
IO.canceled >> IO.println("important")
}
scala> {
import cats.effect.unsafe.implicits.global
program2.unsafeRunSync()
}
important
Within IO.uncancelable { ... }
, cancellation is ignored. To opt back into the cancellation, you can used the passed in poll
function:
lazy val program3 = IO.uncancelable { poll =>
poll(IO.canceled) >> IO.println("nope again")
}
scala> {
import cats.effect.unsafe.implicits.global
program3.unsafeRunSync()
}
java.util.concurrent.CancellationException: Main fiber was canceled
....
IO.uncancelable { ... }
regions are low-level API, and not likely to be used directly.
MonadCancel documentation says:
This means that when writing resource-safe code, we have to worry about cancelation as well as exceptions.
import cats.effect.MonadCancel
lazy val program4 = MonadCancel[IO].bracket(IO.pure(0))(x =>
IO.raiseError(new RuntimeException("boom")))(_ =>
IO.println("cleanup"))
scala> {
import cats.effect.unsafe.implicits.global
program4.unsafeRunSync()
}
cleanup
java.lang.RuntimeException: boom
....
Using MonadCancel[IO].bracket
we can guarantee that the cleanup code will run.
That’s it for today.