package bio
- Source
- package.scala
- Alphabetic
- By Inheritance
- bio
- AnyRef
- Any
- Hide All
- Show All
- Public
- Protected
Type Members
- trait BIOApp extends AnyRef
Safe
Apptype that executes a IO.Safe
Apptype that executes a IO. Shutdown occurs after theIOcompletes, as follows:- If completed with
ExitCode.Success, the main method exits and shutdown is handled by the platform.- If completed with any other
ExitCode,sys.exitis called with the specified code.- If the
IOraises an error, the stack trace is printed to standard error andsys.exit(1)is called.When a shutdown is requested via a signal, the
IOis canceled and we wait for theIOto release any resources. The process exits with the numeric value of the signal plus 128.import cats.effect._ import cats.implicits._ import monix.bio._ object MyApp extends BIOApp { def run(args: List[String]): UIO[ExitCode] = args.headOption match { case Some(name) => UIO(println(s"Hello, \${name}.")).as(ExitCode.Success) case None => UIO(System.err.println("Usage: MyApp name")).as(ExitCode(2)) } }
N.B. this is homologous with cats.effect.IOApp, but meant for usage with IO.
Works on top of JavaScript as well ;-)
- abstract class BiCallback[-E, -A] extends (Either[Cause[E], A]) => Unit
Callback type which supports two channels of errors.
- sealed abstract class Cause[+E] extends Product with Serializable
Represent a complete cause of the failed
IOexposing both typed and untyped error channel. - trait Fiber[E, A] extends cats.effect.Fiber[[β$0$]IO[E, β$0$], A]
Fiberrepresents the (pure) result of a IO being started concurrently and that can be either joined or cancelled.Fiberrepresents the (pure) result of a IO being started concurrently and that can be either joined or cancelled.You can think of fibers as being lightweight threads, a fiber being a concurrency primitive for doing cooperative multi-tasking.
For example a
Fibervalue is the result of evaluating IO.start:val task = UIO.evalAsync(println("Hello!")) val forked: UIO[Fiber[Nothing, Unit]] = task.start
Usage example:
val launchMissiles = Task(println("Missiles launched!")) val runToBunker = Task(println("Run Lola run!")) for { fiber <- launchMissiles.start _ <- runToBunker.onErrorHandleWith { error => // Retreat failed, cancel launch (maybe we should // have retreated to our bunker before the launch?) fiber.cancel.flatMap(_ => Task.raiseError(error)) } aftermath <- fiber.join } yield { aftermath }
- sealed abstract class IO[+E, +A] extends Serializable
Taskrepresents a specification for a possibly lazy or asynchronous computation, which when executed will produce anAas a result, along with possible side-effects.Taskrepresents a specification for a possibly lazy or asynchronous computation, which when executed will produce anAas a result, along with possible side-effects.Compared with
Futurefrom Scala's standard library,Taskdoes not represent a running computation or a value detached from time, asTaskdoes not execute anything when working with its builders or operators and it does not submit any work into any thread-pool, the execution eventually taking place only afterrunAsyncis called and not before that.Note that
Taskis conservative in how it spawns logical threads. Transformations likemapandflatMapfor example will default to being executed on the logical thread on which the asynchronous computation was started. But one shouldn't make assumptions about how things will end up executed, as ultimately it is the implementation's job to decide on the best execution model. All you are guaranteed is asynchronous execution after executingrunAsync.Getting Started
To build a
IOfrom a by-name parameters (thunks), we can use IO.apply ( alias IO.eval), monix.bio.IO.evalTotal if the thunk is guaranteed to not throw any exceptions, or IO.evalAsync:val hello = IO("Hello ") val world = IO.evalAsync("World!")
Nothing gets executed yet, as
IOis lazy, nothing executes until you trigger its evaluation via runAsync or runToFuture.To combine
IOvalues we can use .map and .flatMap, which describe sequencing and this time it's in a very real sense because of the laziness involved:val sayHello = hello .flatMap(h => world.map(w => h + w)) .map(println)
This
IOreference will trigger a side effect on evaluation, but not yet. To make the above print its message:import monix.execution.CancelableFuture import monix.execution.Scheduler.Implicits.global val f = sayHello.runToFuture // => Hello World!
The returned type is a CancelableFuture which inherits from Scala's standard Future, a value that can be completed already or might be completed at some point in the future, once the running asynchronous process finishes. Such a future value can also be canceled, see below.
Laziness, Purity and Referential Transparency
The fact that
Taskis lazy whereasFutureis not has real consequences. For example withTaskyou can do this:import scala.concurrent.duration._ def retryOnFailure[A](times: Int, source: Task[A]): Task[A] = source.onErrorHandleWith { err => // No more retries left? Re-throw error: if (times <= 0) Task.raiseError(err) else { // Recursive call, yes we can! retryOnFailure(times - 1, source) // Adding 500 ms delay for good measure .delayExecution(500.millis) } }
Futurebeing a strict value-wannabe means that the actual value gets "memoized" (means cached), howeverTaskis basically a function that can be repeated for as many times as you want.Taskis a pure data structure that can be used to describe pure functions, the equivalent of Haskell'sIO.Memoization
Taskcan also do memoization, making it behave like a "lazy" ScalaFuture, meaning that nothing is started yet, its side effects being evaluated on the firstrunAsyncand then the result reused on subsequent evaluations:Task(println("boo")).memoizeThe difference between this and just calling
runAsync()is thatmemoize()still returns aTaskand the actual memoization happens on the firstrunAsync()(with idempotency guarantees of course).But here's something else that the
Futuredata type cannot do, memoizeOnSuccess:Task.eval { if (scala.util.Random.nextDouble() > 0.33) throw new RuntimeException("error!") println("moo") }.memoizeOnSuccessThis keeps repeating the computation for as long as the result is a failure and caches it only on success. Yes we can!
WARNING: as awesome as
memoizecan be, use with care because memoization can break referential transparency!Parallelism
Because of laziness, invoking IO.sequence will not work like it does for
Future.sequence, the givenTaskvalues being evaluated one after another, in sequence, not in parallel. If you want parallelism, then you need to use IO.parSequence and thus be explicit about it.This is great because it gives you the possibility of fine tuning the execution. For example, say you want to execute things in parallel, but with a maximum limit of 30 tasks being executed in parallel. One way of doing that is to process your list in batches:
// Some array of tasks, you come up with something good :-) val list: Seq[Task[Int]] = Seq.tabulate(100)(Task(_)) // Split our list in chunks of 30 items per chunk, // this being the maximum parallelism allowed val chunks = list.sliding(30, 30).toSeq // Specify that each batch should process stuff in parallel val batchedTasks = chunks.map(chunk => Task.parSequence(chunk)) // Sequence the batches val allBatches = Task.sequence(batchedTasks) // Flatten the result, within the context of Task val all: Task[Seq[Int]] = allBatches.map(_.flatten)
Note that the built
Taskreference is just a specification at this point, or you can view it as a function, as nothing has executed yet, you need to call runAsync or runToFuture explicitly.Cancellation
The logic described by an
Tasktask could be cancelable, depending on how theTaskgets built.CancelableFuture references can also be canceled, in case the described computation can be canceled. When describing
Tasktasks withTask.evalnothing can be cancelled, since there's nothing about a plain function that you can cancel, but we can build cancelable tasks with IO.cancelable.import scala.concurrent.duration._ import scala.util._ val delayedHello = Task.cancelable0[Unit] { (scheduler, callback) => val task = scheduler.scheduleOnce(1.second) { println("Delayed Hello!") // Signaling successful completion callback(Success(())) } // Returning a cancel token that knows how to cancel the // scheduled computation: Task { println("Cancelling!") task.cancel() } }
The sample above prints a message with a delay, where the delay itself is scheduled with the injected
Scheduler. TheScheduleris in fact an implicit parameter torunAsync().This action can be cancelled, because it specifies cancellation logic. In case we have no cancelable logic to express, then it's OK if we returned a Cancelable.empty reference, in which case the resulting
Taskwould not be cancelable.But the
Taskwe just described is cancelable, for one at the edge, due torunAsyncreturning Cancelable and CancelableFuture references:// Triggering execution val cf = delayedHello.runToFuture // If we change our mind before the timespan has passed: cf.cancel()
But also cancellation is described on
Taskas a pure action, which can be used for example in race conditions:import scala.concurrent.duration._ import scala.concurrent.TimeoutException val ta = Task(1 + 1).delayExecution(4.seconds) val tb = Task.raiseError[Int](new TimeoutException) .delayExecution(4.seconds) Task.racePair(ta, tb).flatMap { case Left((a, fiberB)) => fiberB.cancel.map(_ => a) case Right((fiberA, b)) => fiberA.cancel.map(_ => b) }
The returned type in
racePairis Fiber, which is a data type that's meant to wrap tasks linked to an active process and that can be canceled or joined.Also, given a task, we can specify actions that need to be triggered in case of cancellation, see doOnCancel:
val task = Task.eval(println("Hello!")).executeAsync task doOnCancel IO.evalTotal { println("A cancellation attempt was made!") }
Given a task, we can also create a new task from it that atomic (non cancelable), in the sense that either all of it executes or nothing at all, via uncancelable.
Note on the ExecutionModel
Taskis conservative in how it introduces async boundaries. Transformations likemapandflatMapfor example will default to being executed on the current call stack on which the asynchronous computation was started. But one shouldn't make assumptions about how things will end up executed, as ultimately it is the implementation's job to decide on the best execution model. All you are guaranteed (and can assume) is asynchronous execution after executingrunAsync.Currently the default ExecutionModel specifies batched execution by default and
Taskin its evaluation respects the injectedExecutionModel. If you want a different behavior, you need to execute theTaskreference with a different scheduler. - trait IOLift[F[_]] extends ~>[Task, F]
A lawless type class that specifies conversions from
IOto similar data types (i.e.A lawless type class that specifies conversions from
IOto similar data types (i.e. pure, asynchronous, preferably cancelable).- Annotations
- @implicitNotFound()
- trait IOLike[F[_]] extends ~>[F, Task]
A lawless type class that provides conversions into a IO.
A lawless type class that provides conversions into a IO.
Sample:
// Conversion from cats.Eval import cats.Eval val source0 = Eval.always(1 + 1) val task0 = IOLike[Eval].apply(source0) // Conversion from Future import scala.concurrent.Future val source1 = Future.successful(1 + 1) val task1 = IOLike[Future].apply(source1) // Conversion from IO import cats.effect.{IO => CIO} val source2 = CIO(1 + 1) val task2 = IOLike[CIO].apply(source2)
This is an alternative to the usage of cats.effect.Effect, where the internals are specialized to
IOanyway, like for example the implementation ofmonix.reactive.Observable.- Annotations
- @implicitNotFound()
- final class IOLocal[A] extends AnyRef
A
IOLocalis like a ThreadLocal that is pure and with a flexible scope, being processed in the context of the IO data type.A
IOLocalis like a ThreadLocal that is pure and with a flexible scope, being processed in the context of the IO data type.This data type wraps monix.execution.misc.Local.
Just like a
ThreadLocal, usage of aIOLocalis safe, the state of all current locals being transported over async boundaries (aka when threads get forked) by theTaskrun-loop implementation, but only when theTaskreference gets executed with IO.Options.localContextPropagation set totrue, or it uses a monix.execution.schedulers.TracingScheduler.One way to achieve this is with IO.executeWithOptions, a single call is sufficient just before
runAsync:import monix.execution.Scheduler.Implicits.global val t = Task(42) t.executeWithOptions(_.enableLocalContextPropagation) // triggers the actual execution .runToFuture
Another possibility is to use IO.runToFutureOpt or IO.runToFutureOpt instead of
runAsyncand specify the set of options implicitly:{ implicit val options = IO.defaultOptions.enableLocalContextPropagation // Options passed implicitly val f = t.runToFutureOpt }Full example:
import monix.bio.{UIO, IOLocal} val task: UIO[Unit] = for { local <- IOLocal(0) value1 <- local.read // value1 == 0 _ <- local.write(100) value2 <- local.read // value2 == 100 value3 <- local.bind(200)(local.read.map(_ * 2)) // value3 == 200 * 2 value4 <- local.read // value4 == 100 _ <- local.clear value5 <- local.read // value5 == 0 } yield { // Should print 0, 100, 400, 100, 0 println("value1: " + value1) println("value2: " + value2) println("value3: " + value3) println("value4: " + value4) println("value5: " + value5) } // For transporting locals over async boundaries defined by // Task, any Scheduler will do, however for transporting locals // over async boundaries managed by Future and others, you need // a `TracingScheduler` here: import monix.execution.Scheduler.Implicits.global // Needs enabling the "localContextPropagation" option // just before execution implicit val opts = IO.defaultOptions.enableLocalContextPropagation // Triggering actual execution val result = task.runToFutureOpt
- type Task[+A] = IO[Throwable, A]
Type alias that represents
IOwhich is expected to fail with anyThrowable.Similar tomonix.eval.Taskandcats.effect.IO.Type alias that represents
IOwhich is expected to fail with anyThrowable.Similar tomonix.eval.Taskandcats.effect.IO.WARNING: There are still two error channels (both
Throwable) so use with care. If error is thrown from what was expected to be a pure function (map, flatMap, finalizers, etc.) then it will terminate the Task, instead of a normal failure. - type UIO[+A] = IO[Nothing, A]
Type alias that represents
IOin which all expected errors were handled.
Value Members
- object BiCallback
- object Cause extends Serializable
- object Fiber
- object IO extends TaskInstancesLevel0 with Serializable
Builders for IO.
- object IOLift extends IOLiftImplicits0 with Serializable
- object IOLike extends IOLikeImplicits0 with Serializable
- object IOLocal
Builders for IOLocal
- object Task extends Companion
- object UIO extends Companion