Constructing an IO
An IO can be constructed using IO.delay to capture side effects as an IO value
val stringIO : IO[String] = IO.delay("Hello World!")
Most often, people will use the apply method which internally just calls the delay method.
val stringIO : IO[String] = IO("Hello World!")
Similarly, we could also represent exceptions as IO values by “lift”ing an exception into IO,
as long as we provide the “expected” type of the IO had it succeeded,
either explicitly or through type inference:
val res: IO[String] = IO.raiseError(new Exception("oops !"))
We could also transform futures into IO, which makes it really easy to integrate with legacy codebase:
def futureLong : Future[Long] = ???
val ioFromFuture = IO.fromFuture(IO(futureLong))
It may ask for an implicit context shift, which is provided if run with the cats IOApp instead of the scala native App
Transforming IO:
The IO type is a functor, so we can map over it:
IO(3).map(_ + 1) // IO(4)
It is also an applicative functor, so we can combine and operate on multiple values:
(
IO(3),IO(4),IO(5)).mapN(_ + _ + _) // IO(12)`
It is also a Monad, so we can flatMap over it as well as use it in a for comprehension:
for {
i <- IO(3)
j <- IO(4)
} yield i + j // IO(7)
Error Handling:
Similar to Futures, we can raise and deal with errors in IO:
val result IO.raiseError[Int](new Exception("oops !")) // similar to Future.failed
we can also recover from errors too:
result.handleError(_ => 40) // similar to Future#recover
result.handleErrorWith(_ => IO(40)) // simialr to Future#recoverWith
If you want to your error type to be explicitly seen in the type signature, we cal easily call IO#attempt
which returns an IO[Either[Throwable,A]]:
val attemptedResult : IO[Either[Throwable, Int]] = result.attempt
Error-handling Decision Tree
-
If an error occurs in your IO[A] do you want to…
perform an effect? use:onError(pf: PartialFunction[Throwable, IO[Unit]]): IO[A]
-
transform any error into another error? use
adaptError(pf: PartialFunction[Throwable, Throwable]): IO[A]
-
transform any error into a successful value? use:
handleError(f: Throwable ⇒ A): IO[A]
-
transform some kinds of errors into a successful value? use:
recover(pf: PartialFunction[Throwable, A]): IO[A]
-
transform some kinds of errors into another effect? use:
recoverWith(pf: PartialFunction[Throwable, IO[A]]): IO[A]
-
make errors visible but delay error-handling? use:
attempt: IO[Either[Throwable, A]]
Otherwise, use
handleErrorWith(f: Throwable ⇒ IO[A]): IO[A]
Executing IO Values
We’ve seen that IO values delay or suspend computation, and if we want to run those computations,
we can easily call unsafeRunSync
or unsafeRunAsync
or unsafeToFuture
which is used when we want to convert
from IO
to Future
when integrating with legacy codebase.
But it’s advisable to use the IOApp provided by cats for running Effects.