diff --git a/build.sbt b/build.sbt index ccabd614..9f4e4ae4 100644 --- a/build.sbt +++ b/build.sbt @@ -41,7 +41,7 @@ lazy val benchmarks = project.in(file("benchmarks")) .settings(crossSettings) .settings( libraryDependencies ++= Seq( - "dev.zio" %% "zio" % "1.0.0-RC18-2", + "dev.zio" %% "zio" % "1.0.0-RC21", "io.monix" %% "monix-eval" % monixVersion )) @@ -56,6 +56,12 @@ lazy val docs = project ) .dependsOn(coreJVM) .enablePlugins(DocusaurusPlugin, MdocPlugin, ScalaUnidocPlugin) + .settings( + libraryDependencies ++= Seq( + "dev.zio" %% "zio" % "1.0.0-RC21", + "dev.zio" %% "zio-interop-cats" % "2.1.3.0-RC16", + "io.monix" %% "monix-eval" % monixVersion + )) lazy val mdocSettings = Seq( scalacOptions --= Seq("-Xfatal-warnings", "-Ywarn-unused"), diff --git a/docs/catseffect.md b/docs/catseffect.md new file mode 100644 index 00000000..5b7e3950 --- /dev/null +++ b/docs/catseffect.md @@ -0,0 +1,133 @@ +--- +id: cats-effect +title: Cats-Effect Integration +--- + +`BIO` provides [Cats-Effect](https://github.com/typelevel/cats-effect/) integration out of the box. +In practice, it means that integration with Typelevel libraries, such as [http4s](https://github.com/http4s/http4s), or [doobie](https://github.com/tpolecat/doobie) should work without much hassle. + +## Getting instances in scope + +All Cats instances up until [Effect](https://typelevel.org/cats-effect/typeclasses/effect.html) and [ConcurrentEffect](https://typelevel.org/cats-effect/typeclasses/concurrent-effect.html) (excluded) are available automatically, without any imports. + +```scala mdoc:silent:reset +import cats.syntax.parallel._ +import monix.bio.BIO + +val taskA = BIO(20) +val taskB = BIO(22) + +// evaluates taskA and taskB in parallel, then sums the results +val taskAplusB = (taskA, taskB).parMapN(_ + _) +``` + +`ConcurrentEffect` and `Effect` can be derived if there is `Scheduler` in scope. + +### Sync and above + +Infamous [Sync type class](https://typelevel.org/cats-effect/typeclasses/sync.html) extends `Bracket[F, Throwable]`. +`Throwable` is the error type and as an unfortunate consequence - any type class from `Sync` and above will only work with `BIO[Throwable, A]`. + +For instance, let's say we want to use [monix.catnap.ConcurrentQueue](https://monix.io/api/current/monix/catnap/ConcurrentQueue.html) +which exposes an interface built on Cats-Effect type classes so it can be used with any effect: + +```scala mdoc:silent:reset +import monix.bio.{BIO, Task} +import monix.catnap.ConcurrentQueue + +val queueExample: BIO[Throwable, String] = for { + queue <- ConcurrentQueue[Task].bounded[String](10) + _ <- queue.offer("Message") + msg <- queue.poll +} yield msg +``` + +The `bounded` constructor requires `Concurrent[F]` and `ContextShift[F]` in scope. +Both requirements are automatically derived by `BIO`, but `Concurrent` extends `Sync`, so we need to settle on `Throwable` error type. +Since our `F` is `BIO[Throwable, *]`, all operations on `ConcurrentQueue` will return `BIO[Throwable, *]`. + +A workaround is to use `hideErrors` because these methods don't throw any errors, and even if they did - how would we handle them? + +```scala mdoc:silent:reset +import monix.bio.{Task, UIO} +import monix.catnap.ConcurrentQueue + +val queueExample: UIO[String] = (for { + queue <- ConcurrentQueue[Task].bounded[String](10) + _ <- queue.offer("Message") + msg <- queue.poll +} yield msg).hideErrors +``` + +If typed errors prove to be a great idea in the long term, and not just a temporary fashion, new editions of Cats will likely support it more naturally. + +## Converting from/to other effects + +Cats-Effect provides a hierarchy of type classes that open the door to conversion between effects. + +Monix BIO provides: +- `monix.bio.TaskLike` to convert other effects to `BIO` with nice `BIO.from` syntax. +- `monix.bio.TaskLift` to convert `BIO` to a different type with `bio.to[F]` syntax. + +### cats.effect.IO + +Going from `cats.effect.IO` to `BIO` is very simple because `IO` does not need any runtime to execute: + +```scala mdoc:silent:reset +import monix.bio.BIO + +val io = cats.effect.IO(20) +val bio: BIO[Throwable, Int] = BIO.from(io) +``` + +Unfortunately, we need `Scheduler` in scope to go the other way: + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.Scheduler.Implicits.global + +val bio: BIO[Throwable, Int] = BIO(20) +val ioAgain: cats.effect.IO[Int] = bio.to[cats.effect.IO] +``` + +### monix.eval.Task + +`BIO` does not have `monix.eval.Task` in dependencies, but since they both use `Scheduler` to run, we can do it +without requiring any implicit in scope, if we use `deferAction`: + +```scala mdoc:silent:reset +import monix.bio.BIO + +val task = monix.eval.Task(20) +val bio: BIO[Throwable, Int] = BIO.deferAction(implicit s => BIO.from(task)) +val taskAgain: monix.eval.Task[Int] = monix.eval.Task.deferAction(implicit s => bio.to[monix.eval.Task]) +``` + +In the future, we might introduce a type class in `monix-execution`, which will allow this conversion without any tricks with `deferAction`. + +### zio.ZIO + +To convert from `ZIO`, you will need `ConcurrentEffect[zio.Task]` instance. +It can be derived with [zio-interop-cats](https://github.com/zio/interop-cats) if you have `zio.Runtime` in scope: + +```scala mdoc:silent:reset +import monix.bio.BIO +import zio.interop.catz._ + +implicit val rts = zio.Runtime.default + +val z = zio.ZIO.effect(20) +val bio: BIO[Throwable, Int] = BIO.from(z) +``` + +The other direction requires `Scheduler` in scope: + +```scala mdoc:silent:reset +import monix.bio.BIO +import zio.interop.catz._ + +implicit val s = monix.execution.Scheduler.global + +val bio: BIO[Throwable, Int] = BIO(20) +val zioAgain: zio.Task[Int] = bio.to[zio.Task] +``` \ No newline at end of file diff --git a/docs/creating.md b/docs/creating.md new file mode 100644 index 00000000..418fa6bb --- /dev/null +++ b/docs/creating.md @@ -0,0 +1,324 @@ +--- +id: creating +title: Creating BIO +--- + +As always, a full and up to date list of operators is available in the API or the companion object. + +## Simple builders + +### BIO.now + +`BIO.now` lifts an already known value in the `BIO` context, the equivalent of `Future.successful`. +Do not use it with any side effects, because they will be evaluated immediately and just once: + +```scala mdoc:silent:reset +import monix.bio.BIO + +val task = BIO.now { println("Effect"); "Hello!" } +//=> Effect +``` + +### BIO.raiseError + +`BIO.raiseError` lifts a typed error to the context of `BIO`: + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.exceptions.DummyException +import monix.execution.Scheduler.Implicits.global + +val error: BIO[DummyException, Nothing] = BIO.raiseError(DummyException("boom")) + +error.runAsync(result => println(result)) +//=> Left(Cause.Error(DummyException("boom"))) +``` + +### BIO.terminate + +`BIO.raiseError` lifts a terminal error to the context of `BIO`: + +```scala mdoc:silent:reset +import monix.bio.{BIO, UIO} +import monix.execution.exceptions.DummyException +import monix.execution.Scheduler.Implicits.global + +val error: UIO[Nothing] = BIO.terminate(DummyException("boom")) + +error.runAsync(result => println(result)) +//=> Left(Cause.Termination(DummyException("boom"))) +``` + +### BIO.eval / BIO.apply + +`BIO.eval` is the equivalent of `Function0`, taking a function that will always be evaluated on running, possibly on the same thread (depending on the chosen execution model): + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.Scheduler.Implicits.global + +val task: BIO[Throwable, String] = BIO.eval { println("Effect"); "Hello!" } + +task.runToFuture.foreach(println) +//=> Effect +//=> Hello! + +// The evaluation (and thus all contained side effects) +// gets triggered on each runToFuture: +task.runToFuture.foreach(println) +//=> Effect +//=> Hello! +``` + +`BIO.eval` catches errors that are thrown in the passed function: + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.exceptions.DummyException +import monix.execution.Scheduler.Implicits.global + +val task = BIO.eval { println("Effect"); throw DummyException("Goodbye")} + +task.runAsync(result => println(result)) +//=> Effect +//=> Left(Cause.Error(DummyException("Goodbye"))) + +// The evaluation (and thus all contained side effects) +// gets triggered on each runAsync: +task.runAsync(result => println(result)) +//=> Effect +//=> Left(Cause.Error(DummyException("Goodbye"))) +``` + +### BIO.evalTotal / UIO.apply + +`BIO.evalTotal` is similar to `eval` because it also suspends side effects, but it doesn't expect any errors to be thrown, so the error type is `Nothing`. +If there are any, they are considered terminal errors. + +```scala mdoc:silent:reset +import monix.bio.{BIO, UIO} +import monix.execution.Scheduler.Implicits.global + +val task: UIO[String] = BIO.evalTotal { println("Effect"); "Hello!" } + +task.runToFuture.foreach(println) +//=> Effect +//=> Hello! + +// The evaluation (and thus all contained side effects) +// gets triggered on each runToFuture: +task.runToFuture.foreach(println) +//=> Effect +//=> Hello! +``` + +### BIO.evalOnce + +`BIO.evalOnce` is the equivalent of a `lazy val`, a type that cannot be precisely expressed in Scala. +The `evalOnce` builder does memoization on the first run, such that the result of the evaluation will be available for subsequent runs. +It also has guaranteed idempotency and thread-safety: + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.Scheduler.Implicits.global + +val task = BIO.evalOnce { println("Effect"); "Hello!" } + +task.runToFuture.foreach(println) +//=> Effect +//=> Hello! + +// Result was memoized on the first run! +task.runToFuture.foreach(println) +//=> Hello! +``` + +NOTE: this operation is effectively `BIO.eval(f).memoize`. + +### BIO.never + +`Task.never` returns a Task instance that never completes: + +```scala mdoc:silent:reset +import monix.bio.Task +import monix.execution.Scheduler.Implicits.global +import scala.concurrent.duration._ +import scala.concurrent.TimeoutException + +// A Task instance that never completes +val never = Task.never[Int] + +val timedOut = never.timeoutTo(3.seconds, + Task.raiseError(new TimeoutException)) + +timedOut.runAsync(r => println(r)) +// After 3 seconds: +// => Left(Cause.Error(java.util.concurrent.TimeoutException)) +``` + +This instance is shared so that it can relieve some stress from the garbage collector. + +## Asynchronous builders + +### BIO.evalAsync + +By default, `BIO` prefers to execute things on the current thread. + +`BIO.evalAsync` will evaluate the effect asynchronously; consider it an optimized version of `BIO.eval.executeAsync`. + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.Scheduler.Implicits.global + +BIO.eval(println(s"${Thread.currentThread().getName}: Executing eval")).runSyncUnsafe() +// => main: Executing eval + +BIO.evalAsync(println(s"${Thread.currentThread().getName}: Executing evalAsync")).runSyncUnsafe() +// => scala-execution-context-global-14: Executing evalAsync +``` + +### BIO.create + +`BIO.create` aggregates a handful of methods that create a `BIO` from a callback. + +For example, let's create a utility that evaluates expressions with a given delay: + +```scala mdoc:silent:reset +import monix.bio.BIO +import scala.util.Try +import concurrent.duration._ + +def evalDelayed[A](delay: FiniteDuration) + (f: => A): BIO[Throwable, A] = { + + // On execution, we have the scheduler and + // the callback injected ;-) + BIO.create { (scheduler, callback) => + val cancelable = + scheduler.scheduleOnce(delay) { + callback(Try(f)) + } + + // We must return something that can + // cancel the async computation + cancelable + } +} +``` + +`BIO.create` supports different cancelation tokens, such as: +- `Unit` for non-cancelable tasks +- `cats.effect.IO` +- `monix.bio.BIO` +- `monix.execution.Cancelable` +- And others. + +Some notes: +- Tasks created with this builder are guaranteed to execute asynchronously +- Even if the callback is called on a different thread pool, the resulting task will continue on the default Scheduler. +- The [Scheduler](https://monix.io/docs/3x/execution/scheduler.html) gets injected, and with it, we can schedule things for async execution, we can delay, etc. +- But as said, this callback will already execute asynchronously, so you don’t need to explicitly schedule things to run on the provided Scheduler unless you really need to do it. +- The [Callback](https://monix.io/docs/3x/execution/callback.html) gets injected on execution, and that callback has a contract. In particular, you need to execute `onSuccess`, `onError`, or `onTermination` or apply only once. The implementation does a reasonably good job to protect against contract violations, but if you do call it multiple times, then you’re doing it risking undefined and nondeterministic behavior. +- It’s OK to return a `Cancelable.empty` in case the executed process really can’t be canceled in time. Still, you should strive to produce a cancelable that does cancel your execution, if possible. + +### BIO.fromFuture + +`BIO.fromFuture` can convert any Scala Future instance into a `BIO`: + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.Scheduler.Implicits.global +import scala.concurrent.Future + +val future = Future { println("Effect"); "Hello!" } +val task = BIO.fromFuture(future) +//=> Effect + +task.runToFuture.foreach(println) +//=> Hello! +task.runToFuture.foreach(println) +//=> Hello! +``` + +Note that `fromFuture` takes a strict argument, and that may not be what you want. +When you receive a Future like this, whatever process that’s supposed to complete has probably started already. +You might want a factory of Future to be able to suspend its evaluation and reuse it. +The design of `BIO` is to have fine-grained control over the evaluation model, so in case you want a factory, +you need to either combine it with `BIO.defer` or use `BIO.deferFuture`: + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.Scheduler.Implicits.global +import scala.concurrent.Future + +val task = BIO.defer { + val future = Future { println("Effect"); "Hello!" } + BIO.fromFuture(future) +} + +task.runToFuture.foreach(println) +//=> Effect +//=> Hello! +task.runToFuture.foreach(println) +//=> Effect +//=> Hello! +``` +Or use the equivalent: + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.Scheduler.Implicits.global +import scala.concurrent.Future + +val task = BIO.deferFuture { + Future { println("Effect"); "Hello!" } +} + +task.runToFuture.foreach(println) +//=> Effect +//=> Hello! +task.runToFuture.foreach(println) +//=> Effect +//=> Hello! +``` + +### BIO.deferFutureAction + +`BIO.deferFutureAction` wraps calls that generate Future results into Task, +provided a callback with an injected Scheduler to act as the necessary ExecutionContext. + +This builder helps with wrapping Future-enabled APIs that need an implicit ExecutionContext to work. +Consider this example: + +```scala mdoc:silent:reset +import scala.concurrent.{ExecutionContext, Future} + +def sumFuture(list: Seq[Int])(implicit ec: ExecutionContext): Future[Int] = + Future(list.sum) +``` + +We’d like to wrap this function into one that returns a lazy Task that evaluates this sum every time it is called because that’s how tasks work best. However, to invoke this function, an ExecutionContext is needed: + +```scala mdoc:silent +import monix.bio.Task +import scala.concurrent.ExecutionContext + +def sumTask(list: Seq[Int])(implicit ec: ExecutionContext): Task[Int] = + Task.deferFuture(sumFuture(list)) +``` + +But this is not only superfluous but against the best practices of using `BIO`. +The difference is that Task takes a Scheduler (inheriting from ExecutionContext) only when the run gets called, but we don’t need it just for building a Task reference. +`BIO` is aware that `Scheduler` will be supplied during execution, and it can access it any time. +With `deferFutureAction` or `deferAction` we get to have an injected Scheduler in the passed callback: + +```scala mdoc:silent +import monix.bio.Task + +def sumTask(list: Seq[Int]): Task[Int] = + Task.deferFutureAction { implicit scheduler => + sumFuture(list) + } +``` + +Voilà! No more implicit ExecutionContext passed around. diff --git a/docs/errorhandling.md b/docs/errorhandling.md index 3c819f09..6c8bb390 100644 --- a/docs/errorhandling.md +++ b/docs/errorhandling.md @@ -5,7 +5,7 @@ title: Error Handling When `BIO` fails with an error it short-circuits the computation and returns the error as a result: -```scala mdoc:compile-only +```scala import monix.bio.BIO import monix.execution.exceptions.DummyException import monix.execution.Scheduler.Implicits.global @@ -200,7 +200,7 @@ task.attempt.runSyncUnsafe() `BIO.terminate` can raise a terminal error (second channel with `Throwable`): -```scala mdoc:compile-only +```scala import monix.bio.BIO import monix.execution.Scheduler.Implicits.global import monix.execution.exceptions.DummyException @@ -234,7 +234,7 @@ task.attempt.runSyncUnsafe() If we are sure that our side-effecting code won't have any surprises we can use `BIO.evalTotal` but if we are wrong, the error will be caught in the internal error channel: -```scala mdoc:compile-only +```scala import monix.bio.{BIO, UIO} import monix.execution.Scheduler.Implicits.global import monix.execution.exceptions.DummyException @@ -383,7 +383,7 @@ Terminal errors ignore all typed error handlers and can only be caught by more p The example below shows how `redeemWith` does nothing to handle unexpected errors even if it uses the same type: -```scala mdoc:compile-only +```scala import monix.bio.BIO import monix.execution.exceptions.DummyException import monix.execution.Scheduler.Implicits.global @@ -464,7 +464,7 @@ val task2: BIO[ErrorB, String] = task1.mapError(errA => ErrorB(errA, Instant.now For instance, we might want to log the error without handling it: -```scala mdoc:compile-only +```scala import monix.bio.BIO import monix.execution.exceptions.DummyException import monix.execution.Scheduler.Implicits.global diff --git a/docs/execution.md b/docs/execution.md new file mode 100644 index 00000000..3ac84a7d --- /dev/null +++ b/docs/execution.md @@ -0,0 +1,113 @@ +--- +id: execution +title: Executing BIO +--- + +As mentioned in other sections, `BIO` is lazily evaluated - it needs to be executed to start doing anything. + +## BIOApp + +The ideal way to use `BIO` is to run it only once at the edge of your program, in Main. + +You can go a step further and use `BIOApp` to run the effect for you and prepare a basic environment. +`BIOApp` piggybacks on [IOApp from Cats-Effect](https://typelevel.org/cats-effect/datatypes/ioapp.html) which brings +convenient features like safely releasing resources in case of `Ctrl-C` or `kill` command. + +```scala mdoc:silent +import cats.effect._ +import monix.bio._ + +object Main extends BIOApp { + def run(args: List[String]): UIO[ExitCode] = + args.headOption match { + case Some(name) => + UIO(println(s"Hello, $name.")).map(_ => ExitCode.Success) + case None => + UIO(System.err.println("Usage: MyApp name")).map(_ => ExitCode(2)) + } +} +``` + +## Manual Execution + +If you'd prefer to run manually, there are plenty of options. +This section will cover just a few ones - all variants are prefixed with "run" and should be easy to find in API. +As an optimization, `BIO` will run on the current thread up to the first asynchronous boundary. +You can use `executeAsync` to make sure entire task will run asynchronously. + +Note that all methods to run `BIO` require [Scheduler](https://monix.io/docs/3x/execution/scheduler.html). +Examples will use `Scheduler.global`, which is a good default for most applications. + +### Running to Future + +`BIO.runToFuture` starts the execution and returns a `CancelableFuture`, which will complete when the task finishes. +`CancelableFuture` extends standard `scala.concurrent.Future` and adds an ability to cancel it. +Calling `cancel` will plug into `BIO` cancelation. + +```scala mdoc:silent +import monix.bio.Task +import monix.execution.CancelableFuture +import scala.concurrent.duration._ + +implicit val s = monix.execution.Scheduler.global + +val task = Task(1 + 1).delayExecution(1.second) + +val result: CancelableFuture[Int] = + task.runToFuture + +// If we change our mind +result.cancel() +``` + +All potential errors will be exposed as a failed `Future`. +One gotcha is that `runToFuture` requires error type to be `E <:< Throwable`. +Thankfully, it also applies to `Nothing`, so if we work with typed errors, we can handle error just before running `BIO`. +Probably the most convenient way is to use `attempt`: + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.CancelableFuture + +implicit val s = monix.execution.Scheduler.global + +val task: BIO[Int, Int] = BIO.raiseError(20) + +val result: CancelableFuture[Either[Int, Int]] = + task.attempt.runToFuture +``` + +Typed errors will be exposed as `Left`, and terminal errors will result in a failed `Future`. + +### Running with a callback + +If returning a `Future` is too heavy for your needs, you can use `def runAsync`, which accepts a callback and returns a cancelation token. + +```scala mdoc:silent:reset +import monix.bio.Cause +import monix.bio.Task +import scala.concurrent.duration._ + +implicit val s = monix.execution.Scheduler.global + +val task = Task(1 + 1).delayExecution(1.second) + +val cancelable = task.runAsync { + case Right(value) => + println(value) + case Left(Cause.Error(ex)) => + System.err.println(s"ERROR: ${ex.getMessage}") + case Left(Cause.Termination(ex)) => + System.err.println(s"TERMINATION: ${ex.getMessage}") +} + +// If we change our mind... +cancelable.cancel() +``` + +If your needs are even more modest, use `runAsyncAndForget`, which will run `BIO` in "fire-and-forget" fashion. + +### Blocking for a result + +Monix is [against blocking threads](https://monix.io/docs/3x/best-practices/blocking.html), but when you have to do it, +you can use `runSyncUnsafe`, or `runToFuture` in combination with `Await.result` from the standard library. \ No newline at end of file diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 1bdfb3f7..5386edde 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -3,121 +3,181 @@ id: getting-started title: Getting Started --- -Add the following line to your `build.sbt` +This section briefly covers how to use the essential features of `BIO` 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" % "0.1.0" +libraryDependencies += "io.monix" %% "monix-bio" % "0.1.1" ``` -## Hello world -Let's start with a simple example. Copy and paste it to your favorite -editor and try it yourself. -```scala +Copy and paste it to your favorite editor and try it yourself: + +```mdoc scala:silent import monix.bio.BIO -// Monix uses scheduler instead of execution context +// 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 +// Used for demonstration at the end import scala.concurrent.Await import scala.concurrent.duration._ object HelloWorld { def main(args: Array[String]): Unit = { - // BIO, similarly to Monix's Task is lazy - // nothing gets executed at this point - val bio = BIO { "Hello" + " world!" } - - // we can convert it to execute our bio - // the execution starts here - val normalScalaFuture = bio.runToFuture - - // used only for demonstration, never block - // your threads in production code - val result = Await.result(normalScalaFuture, 5.seconds) - println(result) + // Unlike Future, BIO is lazy and nothing gets executed at this point + val bio = BIO { 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 } } ``` -## Error channels -At this point `BIO` is not much different from `Task` from [Monix library](https://monix.io/docs/3x/eval/task.html). -What makes `BIO` different is the bifunctor concept. -When you take a look at, how is it implemented `sealed abstract class BIO[+E, +A]` you will -see that `BIO` takes two type parameters: -* `E` - our expected error type, indicates what kind of errors are expected when -`BIO` is evaluated -* `A` - it is the result type, it shows what type will be returned -when the computation finishes. +## Composing Tasks -There are two convenience type aliases: -* `type Task[+A] = BIO[Throwable, A]` - represents a `BIO` which uses `Throwable` as the error chanel -* `type UIO[+A] = BIO[Nothing, A]` - represents `BIO` which is not expected to fail +We can combine multiple `BIO` with a variety of functions, the most basic ones being `flatMap` and `map`. -`BIO` has two error channels, one is `E` from the type signature, which indicates an expected error, and the second -one is `Throwable` which is an unexpected error channel. To fail with an unexpected error you can use `BIO.terminate`, -which accepts a `Throwable` and fails the `BIO`. Unexpected error skips -all expected error handlers, except for `BIO.redeemCause` and `BIO.redeemCauseWith`. Even though `UIO` indicates -no expected errors, every `BIO` instance can fail with an unexpected error. +```scala mdoc:silent +import monix.bio.BIO +val hello = BIO("Hello ") +val world = BIO("World!") -Similarly to `Either`, in `BIO` you can transform both: error -and value channel. Please take a look at the example below: +// Will return "Hello World!" after execution +val sayHello = hello + .flatMap(h => world.map(w => h + w)) +``` -```scala -import monix.bio.{BIO, UIO} +`fa.flatMap(a => f(a))` says "execute fa, and if it is successful, map the result to f(a) and execute it". -import monix.execution.Scheduler.Implicits.global +`fa.map(a => f(a))` says "execute fa, and if it is successful, map the result to f(a)". -import scala.concurrent.Await -import scala.concurrent.duration._ +The difference is that `flatMap` expects `A => BIO[E, B]` function, while `map` requires `A => B` so it can return anything. +If we pass `A => BIO[E, B]` to `map`, we will end up with `BIO[E, BIO[E, B]]` which needs to be "flattened" if we want to run the inner `BIO`. -object Bifunctor { - case class Error(description: String) - case class OtherError(description: String) +`BIO` can also return with an error, in which case the computation will short circuit: - def main(args: Array[String]): Unit = { +```scala mdoc:silent +import monix.bio.BIO +import monix.execution.exceptions.DummyException - // BIO.now is a builder from already evaluated value, don't put there anything that can throw - // its similar to Future.successful - val successfulBIO: BIO[Error, String] = BIO.now("Hello world!") - - // You can transform the result value with map and flatMap - val stringLength: BIO[Error, Int] = successfulBIO.map(_.length) - - // BIO.raiseError creates new instance from already evaluated error, again don't put there anything that can throw - // its similar to Future.failed - val failedBIO: BIO[Error, Int] = BIO.raiseError(new Error("Error!")) - - val remappedError: BIO[OtherError, Unit] = failedBIO - // you can map on error, just like on result value - .mapError(err => OtherError(err.description)) - // but if BIO is failed, normal maps wont be executed - // this is the same as with running `mapError` on successful BIO - .map(_ => println("Im never executed!")) - - // sometimes we are not able to handle all the possible cases - // we can terminate our BIO with a fatal, non expected exception - val unexpectedError: UIO[Unit] = BIO.terminate(new Exception("unexpected error")) - - // if we want to run our BIO we need to convert it somehow - we cannot `throw` classes, - // which are not subtypes of throwable. Future, form scala standard library uses Throwable as the error channel, - // so we either need to handle all of our errors or convert them to Throwable. - // We can use attempt which converts our BIO[E, A] into UIO[Either[E, A]] (which means it handles all BIO errors) - val attemptStringLength = stringLength.attempt - println(Await.result(attemptStringLength.runToFuture, 1.second)) - - println(Await.result(remappedError.attempt.runToFuture, 1.second)) - - // as opposed to the example above, the exception is thrown here - // unexpectedError is of type UIO, which indicated that we are not expecting any error, - // but terminate method is used to fail with *unexpected* error - println(Await.result(unexpectedError.attempt.runToFuture, 1.second)) - } +val fa = BIO(println("A")) +val fb = BIO.raiseError(DummyException("boom")) +val fc = BIO(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.](error-handling) + +## Lazy evaluation + +`BIO` 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 `BIO` instances are *values*. +[Here's an explanation why is it cool.](https://www.reddit.com/r/scala/comments/8ygjcq/can_someone_explain_to_me_the_benefits_of_io/e2jfp9b/) +Programming with values helps with composition. +For instance, it is trivial to write a custom retry function: + +```scala mdoc:silent:reset +import monix.bio.BIO +import scala.concurrent.duration._ + +def retryBackoff[E, A](source: BIO[E, A], + maxRetries: Int, firstDelay: FiniteDuration): BIO[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 + BIO.raiseError(ex) + } } ``` +If `BIO` had an eager execution model, it wouldn't be simple to take an arbitrary `BIO` 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](concurrency)) is as simple as calling one of many concurrent operators: + +```scala mdoc:silent:reset +import monix.bio.BIO + +val fa = BIO(println("A")) +val fb = BIO(println("B")) + +// When task is executed, the code +// will print "A" and "B" in non-deterministic order +val task = BIO.parZip2(fa, fb) +``` + +## Cancelation + +`BIO` 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: + +```scala mdoc:silent:reset +import monix.bio.BIO +import monix.execution.exceptions.DummyException +import scala.concurrent.duration._ + +val longRunningTask = BIO.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: + +```scala mdoc:silent:reset +import cats.effect.concurrent.Deferred +import monix.bio.{BIO, Task} +import scala.concurrent.duration._ + +val longRunningTask = BIO.sleep(200.millis) + +val task = + for { + timeoutSignal <- Deferred[Task, Unit] + timeoutCaller = BIO.sleep(50.millis).flatMap(_ => timeoutSignal.complete(())) + _ <- timeoutCaller.startAndForget + _ <- BIO.race(longRunningTask, timeoutSignal.get) + } yield () +``` + +We create [Deferred](https://typelevel.org/cats-effect/concurrency/deferred.html) which is an equivalent of [scala.concurrent.Promise](https://www.scala-lang.org/api/current/scala/concurrent/Promise.html). +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 `BIO`, 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. - +`BIO.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. diff --git a/docs/overview.md b/docs/introduction.md similarity index 97% rename from docs/overview.md rename to docs/introduction.md index bf36ea5d..9ce254c5 100644 --- a/docs/overview.md +++ b/docs/introduction.md @@ -1,6 +1,6 @@ --- -id: overview -title: Overview +id: introduction +title: Introduction --- `BIO[E, A]` represents a specification for a possibly lazy or asynchronous computation. @@ -13,16 +13,16 @@ There are two type aliases: - `type UIO[A] = BIO[Nothing, A]` which represents an effect that can only fail with terminal errors due to abnormal circumstances. - `type Task[A] = BIO[Throwable, A]` - an effect that can fail with a `Throwable` and is analogous to `monix.eval.Task`. -[More about errors here.](error-handling) - `Monix BIO` builds upon [Monix Task](https://monix.io/api/3.2/monix/eval/Task.html) and enhances it with typed error capabilities. If you are already familiar with `Task` - learning `BIO` is straightforward because the only difference is in error handling - the rest of API is the same. In many cases, migration might be as simple as changing imports from `monix.eval.Task` to `monix.bio.Task`. +[Go here if you're looking to get started as quickly as possible.](getting-started) + ## Usage Example -```scala mdoc:compile-only +```scala mdoc:silent import monix.bio.{BIO, UIO} import monix.execution.CancelableFuture import scala.concurrent.duration._ diff --git a/docs/resourcesafety.md b/docs/resourcesafety.md index 86fd36c3..f1982cc5 100644 --- a/docs/resourcesafety.md +++ b/docs/resourcesafety.md @@ -40,9 +40,7 @@ and supports concurrency and cancellation. ```scala mdoc:compile-only import java.io._ -import cats.effect.ExitCase import monix.bio.{Task, UIO} -import monix.bio.Cause def readFirstLine(file: File): Task[String] = { val acquire = Task(new BufferedReader(new FileReader(file))) @@ -159,7 +157,6 @@ It is allowed to nest, or install multiple finalizers: ```scala mdoc:silent:reset import monix.bio.UIO import monix.execution.Scheduler.Implicits.global -import monix.execution.exceptions.DummyException UIO(println("action")) .guarantee( diff --git a/website/i18n/en.json b/website/i18n/en.json index 280121e5..587b23be 100644 --- a/website/i18n/en.json +++ b/website/i18n/en.json @@ -5,17 +5,26 @@ "previous": "Previous", "tagline": "Asynchronous Programming for Scala and Scala.js", "docs": { + "cats-effect": { + "title": "Cats-Effect Integration" + }, "comparison": { "title": "Other Effects" }, + "creating": { + "title": "Creating BIO" + }, "error-handling": { "title": "Error Handling" }, + "execution": { + "title": "Executing BIO" + }, "getting-started": { "title": "Getting Started" }, - "overview": { - "title": "Overview" + "introduction": { + "title": "Introduction" }, "resource-safety": { "title": "Resource Safety" diff --git a/website/sidebars.json b/website/sidebars.json index 206c1592..88b855c4 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -1,5 +1,5 @@ { "docs": { - "Documentation": ["overview", "getting-started", "error-handling", "resource-safety", "comparison"] + "Documentation": ["introduction", "getting-started", "creating", "execution", "error-handling", "resource-safety", "cats-effect", "comparison"] } } \ No newline at end of file diff --git a/website/siteConfig.js b/website/siteConfig.js index e048ed1a..ab732ffe 100644 --- a/website/siteConfig.js +++ b/website/siteConfig.js @@ -15,7 +15,7 @@ const siteConfig = { headerLinks: [ { href: apiUrl, label: "API Docs" }, - { doc: "overview", label: "Documentation" }, + { doc: "introduction", label: "Documentation" }, { href: repoUrl, label: "GitHub" } ],