raymondtay.github.io

Blogging site

Journey into the IO Monad

The IO monad in Scala, that is. Not Haskell’s IO monad. This story is about how i got started with cats-effect and why i eventually went along with it. When this project was started by Daniel Spiewak and later on fleshed out into its current form by many contributors (including the author(s) of Monix), i did not quite know anything about it and why i should start using it and i viewed Daniel’s talk about this several times.

Why i think cats-effect is a really nice library because it provides the developer abstractions which allows the developer to describe the following IO interactions which is lazily evaluated & stack-safe (via trampolines):

And the best part is that this library was designed to allow other developers to provide their interpretation of these IO interactions though cats-effect provides its own default interpreter; that is to say other implementors could potentially provide actual implementations e.g. ZIO and its IO interpreter.

I started by understanding what went under the hood when i wrote something like this:

scala> println("Hello World") // evaluated eagerly
scala> IO(println("Hello World")) // "println(..)" is suspended in the IO

If you know some Scala, you will realize quickly that the former expression is strictly evaluated and i cannot delay its execution, not even with scala.concurrent.Future


Scenarios You can Use with Cats-Effect

A problem that cats-effect was created to solve IO actions (i.e. computations) to be raced concurrently and the losers of the race would be safely deallocated. The solution to a problem i was handling was to think about how i could modelled the capture of the http 1.x client from the library i used (i.e. akka-http) and the critical thing to realize is to understand the general structure which follows the idea of using a Resource with error-handling code after plugging its acquire-release handlers. This is best encapsulated by the type signature below:

class Resource[F[_], A] {
  // safely create and release a resource
  def make[F[_]:, A](acquire: F[A])(release: A => F[Unit])(implicit F: Functor[F]): Resource[F, A]
  // use a resource with error-handling
  def use[B](f: A => F[B])(implicit F: Bracket[F, Throwable]): F[B] }

Based on the above type signature, you might notice that there’s an implicit value F and you might be wondering what is that? The F represents the implementation that will handle errors encountered (you would know this because Bracket represents behaviors which allows you to deal with errors, represented here by Throwable) and that implementation is the IO. If you like to read the entire approach, follow the link ⇒(full source code)[]:

// Creates a resource that uses akka-http client
def makeHttp1xResource = {
  val acquire = IO(Http())
  def release(http1x : HttpExt) = IO.unit /* nothing to release. */    
  Resource.make(acquire)(release)
}
// Uses the resource, makes a http call and prints the Http status code else in error prints error
def requestHttp1x(client: HttpExt, site: String) : IO[Unit] = {
  IO.fromFuture(IO(client.singleRequest(HttpRequest(uri = site))))
    .flatMap(result => IO(println(result.status)))
    .handleErrorWith(error => IO(println("Error"))) *> IO.unit
}

When you wish to execute it sequentially, perhaps you have a smaller capacity machine or VM, here’s how you do it by invoking unsafeRunSync:

lazy val responses0 : IO[List[Unit]] =
  sites traverse (site => makeHttp1xResource.use(client => requestHttp1x(client, site))) // does not execute
...
responses0.unsafeRunSync // finally executes the crawling of sites

If there are more resources available, you can make a minor alteration to the code by invoking the presents of the Parallel Monad and watch it execute in parallel:

lazy val responses2 : IO[List[Unit]] = {
  import IO._ /* bring in the Parallel[IO] implicit instances */
  sites parTraverse (site => makeHttp1xResource.use(client => requestHttp1x(client, site)))
}

Links to other related posts: