For few days I have been wrapping my head around cats-effect and IO. And I feel I have some misconceptions about this effect or simply I missed its point.
IO.shift
? Using IO.async
? Is IO.delay
sync or async? Can we make a generic async task with code like this Async[F].delay(...)
? Or async happens when we call IO with unsafeToAsync
or unsafeToFuture
?I would appreciate some clarifing on any of this as I have failed comprehending cats-effect docs on those and internet was not that helpfull...
A data type for encoding side effects as pure values, capable of expressing both synchronous and asynchronous computations.
Cats is a library which provides abstractions for functional programming in the Scala programming language. Scala supports both object-oriented and functional programming, and this is reflected in the hybrid approach of the standard library.
Although Scala doesn't have an effects system, its type system is strong enough to allow us to write one as a library, with the limitation that the compiler won't enforce its proper use. Multiple libraries like ZIO, Monix IO, and Cats Effects attempt to provide an effects system for Scala.
if IO can replace Scala's Future, how can we create an async IO task
First, we need to clarify what is meant as an async task. Usually async means "does not block the OS thread", but since you're mentioning Future
, it's a bit blurry. Say, if I wrote:
Future { (1 to 1000000).foreach(println) }
it would not be async, as it's a blocking loop and blocking output, but it would potentially execute on a different OS thread, as managed by an implicit ExecutionContext. The equivalent cats-effect code would be:
for { _ <- IO.shift _ <- IO.delay { (1 to 1000000).foreach(println) } } yield ()
(it's not the shorter version)
So,
IO.shift
is used to maybe change thread / thread pool. Future
does it on every operation, but it's not free performance-wise.IO.delay
{ ... } (a.k.a. IO { ... }
) does NOT make anything async and does NOT switch threads. It's used to create simple IO
values from synchronous side-effecting APIsNow, let's get back to true async. The thing to understand here is this:
Every async computation can be represented as a function taking callback.
Whether you're using API that returns Future
or Java's CompletableFuture
, or something like NIO CompletionHandler
, it all can be converted to callbacks. This is what IO.async
is for: you can convert any function taking callback to an IO
. And in case like:
for { _ <- IO.async { ... } _ <- IO(println("Done")) } yield ()
Done
will be only printed when (and if) the computation in ...
calls back. You can think of it as blocking the green thread, but not OS thread.
So,
IO.async
is for converting any already asynchronous computation to IO
.IO.delay
is for converting any completely synchronous computation to IO
.The closest analogy when working with Future
s is creating a scala.concurrent.Promise
and returning p.future
.
Or async happens when we call IO with unsafeToAsync or unsafeToFuture?
Sort of. With IO
, nothing happens unless you call one of these (or use IOApp
). But IO does not guarantee that you would execute on a different OS thread or even asynchronously unless you asked for this explicitly with IO.shift
or IO.async
.
You can guarantee thread switching any time with e.g. (IO.shift *> myIO).unsafeRunAsyncAndForget()
. This is possible exactly because myIO
would not be executed until asked for it, whether you have it as val myIO
or def myIO
.
You cannot magically transform blocking operations into non-blocking, however. That's not possible neither with Future
nor with IO
.
What's the point of Async and Concurrent in cats-effect? Why they are separated?
Async
and Concurrent
(and Sync
) are type classes. They are designed so that programmers can avoid being locked to cats.effect.IO
and can give you API that supports whatever you choose instead, such as monix Task or Scalaz 8 ZIO, or even monad transformer type such as OptionT[Task, *something*]
. Libraries like fs2, monix and http4s make use of them to give you more choice of what to use them with.
Concurrent
adds extra things on top of Async
, most important of them being .cancelable
and .start
. These do not have a direct analogy with Future
, since that does not support cancellation at all.
.cancelable
is a version of .async
that allows you to also specify some logic to cancel the operation you're wrapping. A common example is network requests - if you're not interested in results anymore, you can just abort them without waiting for server response and don't waste any sockets or processing time on reading the response. You might never use it directly, but it has it's place.
But what good are cancelable operations if you can't cancel them? Key observation here is that you cannot cancel an operation from within itself. Somebody else has to make that decision, and that would happen concurrently with the operation itself (which is where the type class gets its name). That's where .start
comes in. In short,
.start
is an explicit fork of a green thread.
Doing someIO.start
is akin to doing val t = new Thread(someRunnable); t.start()
, except it's green now. And Fiber
is essentially a stripped down version of Thread
API: you can do .join
, which is like Thread#join()
, but it does not block OS thread; and .cancel
, which is safe version of .interrupt()
.
Note that there are other ways to fork green threads. For example, doing parallel operations:
val ids: List[Int] = List.range(1, 1000) def processId(id: Int): IO[Unit] = ??? val processAll: IO[Unit] = ids.parTraverse_(processId)
will fork processing all IDs to green threads and then join them all. Or using .race
:
val fetchFromS3: IO[String] = ??? val fetchFromOtherNode: IO[String] = ??? val fetchWhateverIsFaster = IO.race(fetchFromS3, fetchFromOtherNode).map(_.merge)
will execute fetches in parallel, give you first result completed and automatically cancel the fetch that is slower. So, doing .start
and using Fiber
is not the only way to fork more green threads, just the most explicit one. And that answers:
Is IO a green thread? If yes, why is there a Fiber object in cats-effect? As I understand the Fiber is the green thread, but docs claim we can think of IOs as green threads.
IO
is like a green thread, meaning you can have lots of them running in parallel without overhead of OS threads, and the code in for-comprehension behaves as if it was blocking for the result to be computed.
Fiber
is a tool for controlling green threads explicitly forked (waiting for completion or cancelling).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With