Blogging site
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
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: