Monix BIO

Monix BIO

  • API Docs
  • Documentation
  • GitHub

›Documentation

Documentation

  • Introduction
  • Getting Started
  • Creating IO
  • Executing IO
  • Error Handling
  • Resource Safety
  • Cats-Effect Integration
  • Asynchronous Stack Traces
  • Other Effects

Getting Started

This section briefly covers how to use the essential features of IO and should get you started quickly. Refer to the rest of the documentation for a more in-depth description.

Hello World

Let's start with a simple example. Add the following line to your build.sbt:

libraryDependencies += "io.monix" %% "monix-bio" % "1.0.0"

Copy and paste it to your favorite editor and try it yourself:

import monix.bio.IO
// Monix uses scheduler instead of the execution context
// from scala standard library, to learn more check
// https://monix.io/docs/3x/execution/scheduler.html
import monix.execution.Scheduler.Implicits.global

// Used for demonstration at the end
import scala.concurrent.Await
import scala.concurrent.duration._

object HelloWorld {

  def main(args: Array[String]): Unit = {
    // Unlike Future, IO is lazy and nothing gets executed at this point
    val bio = IO { println("Hello, World!"); "Hi" }
    
    // We can run it as a Scala's Future
    val scalaFuture: Future[String] = bio.runToFuture

    // Used only for demonstration, 
    // think three times before you use `Await` in production code!
    val result: String = Await.result(scalaFuture, 5.seconds)

    println(result) // Hi is printed
  }
}

Composing Tasks

We can combine multiple IO with a variety of functions, the most basic ones being flatMap and map.

import monix.bio.IO

val hello = IO("Hello ")
val world = IO("World!")

// Will return "Hello World!" after execution
val sayHello = hello
  .flatMap(h => world.map(w => h + w))

fa.flatMap(a => f(a)) says "execute fa, and if it is successful, map the result to f(a) and execute it".

fa.map(a => f(a)) says "execute fa, and if it is successful, map the result to f(a)".

The difference is that flatMap expects A => IO[E, B] function, while map requires A => B so it can return anything. If we pass A => IO[E, B] to map, we will end up with IO[E, IO[E, B]] which needs to be "flattened" if we want to run the inner IO.

IO can also return with an error, in which case the computation will short circuit:

import monix.bio.IO
import monix.execution.exceptions.DummyException

val fa = IO(println("A"))
val fb = IO.raiseError(DummyException("boom"))
val fc = IO(println("C"))

// Will print A, then throw DummyException after execution
val task = fa.flatMap(_ => fb).flatMap(_ => fc)

In the above example, fc is never executed. We can recover from errors using functions such as onErrorHandleWith. Check Error Handling section for an in-depth overview.

Lazy evaluation

IO is just a description or a factory of an effect. It won't do anything until it is executed through runXYZ methods. In more functional terms, it means that IO instances are values. Here's an explanation why is it cool.

Programming with values helps with composition. For instance, it is trivial to write a custom retry function:

import monix.bio.IO
import scala.concurrent.duration._

def retryBackoff[E, A](source: IO[E, A],
  maxRetries: Int, firstDelay: FiniteDuration): IO[E, A] = {

  source.onErrorHandleWith { ex =>
      if (maxRetries > 0)
        // Recursive call, it's OK as Monix is stack-safe
        retryBackoff(source, maxRetries - 1, firstDelay * 2)
          .delayExecution(firstDelay)
      else
        IO.raiseError(ex)
  }
}

If IO had an eager execution model, it wouldn't be simple to take an arbitrary IO and then enhance it with new behavior. We would have to be extra careful not to pass an already running task, which is error-prone.

Concurrency

Running two tasks concurrently, or in parallel (learn the difference in concurrency section) is as simple as calling one of many concurrent operators:

import monix.bio.IO

val fa = IO(println("A"))
val fb = IO(println("B"))

// When task is executed, the code 
// will print "A" and "B" in non-deterministic order
val task = IO.parZip2(fa, fb)

Cancelation

IO supports cancelation, which means that it will be stopped at the nearest possible opportunity. Canceled tasks turn into non-terminating, which means they will never signal any result. You can signal an error instead if you use onCancelRaiseError.

One of the use cases for cancelation is a timeout:

import monix.bio.IO
import monix.execution.exceptions.DummyException
import scala.concurrent.duration._

val longRunningTask = IO.sleep(200.millis)

longRunningTask.timeoutWith(100.millis, DummyException("Timed out!"))

Combining high-level blocks

We can combine different pieces to create more complex behavior. Let's develop a simple dynamic timeout:

import cats.effect.concurrent.Deferred
import monix.bio.{IO, Task}
import scala.concurrent.duration._

val longRunningTask = IO.sleep(200.millis)

val task =
  for {
    timeoutSignal <- Deferred[Task, Unit]
    timeoutCaller = IO.sleep(50.millis).flatMap(_ => timeoutSignal.complete(()))
    _ <- timeoutCaller.startAndForget
    _ <- IO.race(longRunningTask, timeoutSignal.get)
  } yield ()

We create Deferred which is an equivalent of scala.concurrent.Promise. It can be completed at most once with complete(), but it can be read many times with get. If the value is not yet available, get will block asynchronously (without blocking any underlying thread, it will just suspend IO, which is extremely cheap in comparison) until it is there. timeoutCaller represents a task which signals a timeout from a different part of the program.

We can use startAndForget to run it concurrently with subsequent operations. This method starts the computation in the background and then returns without waiting for the result.

IO.race runs two tasks concurrently, and once any of them completes, the other one is canceled. If we race longRunningTask against timeoutSignal.get, it will be canceled once the signal is sent.

← IntroductionCreating IO →
  • Hello World
  • Composing Tasks
  • Lazy evaluation
  • Concurrency
  • Cancelation
  • Combining high-level blocks

Copyright © 2019-2021 The Monix Project Developers.