{"id":23924655,"url":"https://github.com/rcardin/yaes","last_synced_at":"2026-05-03T17:06:10.664Z","repository":{"id":270458815,"uuid":"890225578","full_name":"rcardin/yaes","owner":"rcardin","description":"An experimental effect system in Scala 3 that tracks effects through context functions","archived":false,"fork":false,"pushed_at":"2026-02-16T08:43:19.000Z","size":56524,"stargazers_count":122,"open_issues_count":15,"forks_count":6,"subscribers_count":7,"default_branch":"main","last_synced_at":"2026-02-16T16:22:20.346Z","etag":null,"topics":["algebraic-effects","direct-style","effect-system","monad","scala3"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rcardin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"docs/contributing.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-11-18T07:57:42.000Z","updated_at":"2026-02-11T16:51:12.000Z","dependencies_parsed_at":"2025-01-21T14:29:09.974Z","dependency_job_id":"5c98d5ce-f009-409a-866a-beb9f39bd832","html_url":"https://github.com/rcardin/yaes","commit_stats":null,"previous_names":["rcardin/yaes"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/rcardin/yaes","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rcardin%2Fyaes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rcardin%2Fyaes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rcardin%2Fyaes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rcardin%2Fyaes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rcardin","download_url":"https://codeload.github.com/rcardin/yaes/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rcardin%2Fyaes/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29559961,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T21:50:49.831Z","status":"ssl_error","status_checked_at":"2026-02-17T21:46:15.313Z","response_time":100,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["algebraic-effects","direct-style","effect-system","monad","scala3"],"created_at":"2025-01-05T19:16:38.115Z","updated_at":"2026-05-03T17:06:10.655Z","avatar_url":"https://github.com/rcardin.png","language":"Scala","funding_links":[],"categories":["\u003ca name=\"Scala\"\u003e\u003c/a\u003eScala"],"sub_categories":[],"readme":"![Made for Scala 3](https://img.shields.io/badge/Scala%203-%23de3423.svg?logo=scala\u0026logoColor=white)\n![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/rcardin/yaes/scala.yml?branch=main)\n![Maven Central](https://img.shields.io/maven-central/v/in.rcard.yaes/yaes-core_3)\n![GitHub release (latest by date)](https://img.shields.io/github/v/release/rcardin/yaes)\n[![javadoc](https://javadoc.io/badge2/in.rcard.yaes/yaes-core_3/javadoc.svg)](https://javadoc.io/doc/in.rcard.yaes/yaes-core_3)\n\n# Yet Another Effect System (λÆS)\n\u003cimg align=\"right\" src=\"./logo.svg\" width=\"230\" alt=\"logo\" /\u003e\n\nλÆS is an experimental effect system in Scala inspired by the ideas behind Algebraic Effects. Using Scala 3 [context parameters](https://docs.scala-lang.org/scala3/reference/contextual/using-clauses.html) and [context functions](https://docs.scala-lang.org/scala3/reference/contextual/context-functions.html), it provides a way to define and handle effects in a modular and composable manner.\n\nYou can visit the dedicated [website](https://rcardin.github.io/yaes/) 🌐.\n\nHere is the talk I gave at the **Scalar 2025** about the main concepts behind the library:\n\u003cbr clear=\"both\" /\u003e\n\n[![Watch the video](https://img.youtube.com/vi/TXUxCsPpZp0/maxresdefault.jpg)](https://youtu.be/TXUxCsPpZp0)\n\nAvailable modules are:\n * `yaes-core`: The main effects of the λÆS library.\n * `yaes-data`: A set of data structures that can be used with the λÆS library.\n * `yaes-cats`: Integration with Cats and Cats Effect, providing interoperability and typeclass instances.\n * `yaes-slf4j`: SLF4J integration for the `Log` effect, enabling any SLF4J-compatible logging backend.\n\nWhat's new in λÆS when compared to other effect systems? Well, λÆS embraces direct style — no monads, no for-comprehensions, just plain Scala:\n\n```scala 3\nimport in.rcard.yaes.Random.*\nimport in.rcard.yaes.Raise.*\n\ndef drunkFlip(using Random, Raise[String]): String = {\n  val caught = Random.nextBoolean\n  if (caught) {\n    val heads = Random.nextBoolean\n    if (heads) \"Heads\" else \"Tails\"\n  } else {\n    Raise.raise(\"We dropped the coin\")\n  }\n}\n```\n\nIn λÆS types like `Random` and `Raise` are *Effects*. A *Side Effect* is an unpredictable interaction, usually with an external system. An Effect System manages *Side Effects* by tracking and wrapping them into *Effects*. An *Effect* describes the type of the *Side Effect* and the return type of an effectful computation. We manage *Side Effect* behavior by putting them in a kind of box.\nCalling the above `drunkFlip` function will not execute the effects. Instead, it will return a value that represents something that can be run but hasn’t yet. This is called deferred execution. \n\nAn Effect System provides all the tools to manage and execute Effectful computations in a deferred manner. In λÆS, such tools are called *Handlers*.\n\n```scala 3\nimport in.rcard.yaes.Random.*\nimport in.rcard.yaes.Raise.*\n\nval result: String = Raise.run { \n  Random.run { \n    drunkFlip\n  }\n}\n```\n\nIn the above code, we are running the `drunkFlip` function with the `Random` and `Raise` effects. The `Raise.run` and `Random.run` functions are defined using *Handlers* that will execute the deferred effects. The approach reminds the one defined in the Algebraic Effects and Handlers theory. The example shows how to handle the `Raise` and `Random` effects one at a time. However, we're free to handle only one effect at a time:\n\n```scala 3\nimport in.rcard.yaes.Random.*\n\nval result: Raise[String] ?=\u003e String = Random.run { \n  drunkFlip\n}\n```\n\nThe above code shows how to handle only the `Random` effect. The `Raise` effect is still present. It's a powerful feature that allows for a fine-grained management of the effects.\n\n## Dependency\n\nThe library is available on Maven Central. To use it, add the following dependency to your build.sbt files:\n\n**For effects only** (Raise, Async, Sync, etc.):\n\n```sbt\nlibraryDependencies += \"in.rcard.yaes\" %% \"yaes-core\" % \"0.18.0\"\n```\n\n**For effects + data structures** (Flow, Channel, and reactive streams):\n\n```sbt\nlibraryDependencies += \"in.rcard.yaes\" %% \"yaes-data\" % \"0.18.0\"\n```\n\n**For Cats integration** (includes all effects and data structures):\n\n```sbt\nlibraryDependencies += \"in.rcard.yaes\" %% \"yaes-cats\" % \"0.18.0\"\n```\n\n**For SLF4J logging integration** (delegates `Log` effect to any SLF4J backend):\n\n```sbt\nlibraryDependencies += \"in.rcard.yaes\" %% \"yaes-slf4j\" % \"0.18.0\"\n```\n\n**For HTTP Server based on λÆS effects**:\n\n```sbt\nlibraryDependencies += \"in.rcard.yaes\" %% \"yaes-http-server\" % \"0.18.0\"\n```\n\nThe library is only available for Scala 3 and is currently in an experimental stage. The API is subject to change.\n\n### Requirements\n\n- **Java 24 or higher** is required to run λÆS due to its use of modern Java features like Virtual Threads and Structured Concurrency.\n\n## Usage\n\nThe library provides a set of effects and handlers that can be used to define and handle effectful computations. The available effects are:\n\n- [`Sync`](#the-sync-effect): Allows for running side-effecting operations.\n- [`Async`](#the-async-effect): Allows for asynchronous computations and fiber management.\n- [`Raise`](#the-raise-effect): Allows for raising and handling errors.\n- [`Resource`](#the-resource-effect): Allows for automatic resource management with guaranteed cleanup.\n- [`Shutdown`](#the-shutdown-effect): Allows for graceful shutdown coordination with callback hooks.\n- [`Input`](#the-input-effect): Allows for reading input from the console.\n- [`Output`](#the-output-effect): Allows for printing output to the console.\n- [`Random`](#the-random-effect): Allows for generating random content.\n- [`Clock`](#the-clock-effect): Allows for managing time.\n- [`System`](#the-system-effect): Allows for managing system properties and environment variables.\n- [`State`](#the-state-effect): Allows for stateful computations in a purely functional manner.\n- [`Writer`](#the-writer-effect): Allows for pure, append-only value accumulation.\n- [`Reader`](#the-reader-effect): Allows for read-only access to environment values.\n- [`Log`](#the-log-effect): Allows for logging messages at different levels.\n\nThe library also provides the following handlers that orchestrate existing effects:\n\n- [`Retry`](#the-retry-handler): Retries failing blocks according to composable schedule policies.\n\n### YaesApp: Common Entry Point\n\nFor building complete applications, λÆS provides `YaesApp`, a trait that simplifies application development by automatically handling common effects in the correct order.\n\n**Quick Example:**\n\n```scala 3\nimport in.rcard.yaes.*\n\nobject MyApp extends YaesApp {\n  override def run {\n    Output.printLn(s\"Hello! Starting with args: ${args.mkString(\", \")}\")\n\n    val currentTime = Clock.now\n    Output.printLn(s\"Current time: $currentTime\")\n\n    val randomNumber = Random.nextInt\n    Output.printLn(s\"Random number: $randomNumber\")\n  }\n}\n```\n\n`YaesApp` automatically provides:\n- **Sync** - Tracking side-effecting computations\n- **Output**, **Input** - Console I/O\n- **Random** - Random number generation\n- **Clock** - Time operations\n- **System** - System properties and environment variables\n\nFor more details, see the [YaesApp documentation](docs/yaes-app.md).\n\n### The `Sync` Effect\n\nThe `Sync` effect allows for running side-effecting operations:\n\n```scala 3\nimport in.rcard.yaes.Sync.*\n\ncase class User(name: String)\n\ndef saveUser(user: User)(using Sync): Long =\n  throw new RuntimeException(\"Read timed out\")\n```\n\nThe above code can throw an uncontrolled exception if the connection with the database times out. The generic `Sync` effect lift the function in the world of the effectful computations, making it referentially transparent. It means that everything that is not referentially transparent should be defined using the `Sync` effect. In fact, the `Sync` effect provides a guard rail to uncontrolled exceptions since its handler returns always a monad that wraps the result of the effectful computation.\n\n\nTo run the effectful computation, we can use the provided handlers.\n\nThe first handler doesn't block the current thread:\n\n```scala 3\nimport in.rcard.yaes.Sync.*\n\nimport scala.concurrent.Future\nimport scala.concurrent.ExecutionContext.Implicits.global\n\nval result: Future[Long] = Sync.run {\n  saveUser(User(\"John\"))\n}\n```\n\nThe library also provides a blocking handler that will block the current thread until the effectful computation is finished:\n\n```scala 3\nimport in.rcard.yaes.Sync.*\n\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.duration.DurationInt\nimport scala.util.Try\n\nval result: Long = Sync.runBlocking(2.seconds) {\n  saveUser(User(\"John\"))\n}\n```\n\nPlease, be aware that running a `Sync` effectful computation both using the `Sync.run` and `Sync.runBlocking` methods breaks the referential transparency. Handlers should be used only at the edge of the application.\n\nThe default `Sync` handler is implemented using Java Virtual Threads machinery. For every effectful computation, a new virtual thread is created and the computation is executed in that thread. \n\n### The `Async` Effect\n\nThe `Async` effect is built around the ideas developed in the [Sus4s](https://github.com/rcardin/sus4s) library. It allows for running asynchronous computations and managing fibers.\n\nThe default implementation of the `Async` effect is based Java Structured Concurrency provided by Java versions after 21. The `Async` effect provides a way to define asynchronous computations that are executed in a structured way. It means that every asynchronous computation is executed in a fiber that is managed by the `Async` effect.\n\nThe most important operation of the `Async` effect is the `fork` operation:\n\n```scala 3\nimport in.rcard.yaes.Async.*\n\ndef findUserByName(name: String): Option[User] = Some(User(name))\nval fb: Async ?=\u003e Fiber[Option[User]] = Async.fork { findUserByName(\"John\") }\n```\n\nThe `fb` variable represent a fiber (lightweight thread) that is executing the `findUserByName` function. The `fork` operation returns a `Fiber` object that can be used to manage the execution of the asynchronous computation. In details, we can wait for the value of the computation using the `value` operation:\n\n```scala 3\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\nval maybeUser: (Async, Raise[Cancelled]) ?=\u003e Option[User] = fb.value\n```\n\nOr, we can just wait for the computation to finish:\n\n```scala 3\nval p: Async ?=\u003e Option[User] = fb.join()\n```\n\nAs for the `Sync` effect, forking a new fiber or joining it doesn't execute the effectful computation. It just returns a value that represents the computation that can be run but hasn't yet.\n\nAgain, we can run the effectful computation using the provided handlers:\n\n```scala 3\nimport in.rcard.yaes.Async.*\n\nval maybeUser: Raise[Cancelled] ?=\u003e Option[User] = Async.run {\n    val fb: Async ?=\u003e Fiber[Option[User]] = Async.fork { findUserByName(\"John\") }\n    fb.value\n  }\n```\n\nThe above code shows another important aspect of the λÆS library. We can handle an effect eliminating it from the list of effects one at time. In the above code, we are handling the `Async` effect first, and we remain with the `Raise` effect. It's a powerful feature that allows for a fine-grained management of the effects.\n\nThe `Async` effect is transparent to possible exceptions thrown by the effectful computation. Please, add the `Sync` effect if you think the effectful computation can throw any exception.\n\n#### Structured Concurrency\n\nThe `Async` effect implements **structured concurrency**. The `Async.run` handler creates a new structured concurrency scope where all the fibers are executed. The `Async.run` will wait for all the fibers to finish before returning the result of the effectful computation both if the fibers are joined or not.\n\n```scala 3\nimport in.rcard.yaes.Async.*\n\ndef updateUser(user: User): Unit                = ???\ndef updateClicks(user: User, clicks: Int): Unit = ???\n\nAsync.run {\n  val john = User(\"John\")\n  Async.fork {\n    updateUser(john)\n  }\n  Async.fork {\n    updateClicks(john, 10)\n  }\n}\n```\n\nThe `Async.run` function will wait for both the `updateUser` and `updateClicks` functions to finish before returning. It's a powerful feature that allows for a structured way to manage the execution of asynchronous computations.\n\nAnother important feature of strutctured concurrency is the *cancellation* of the fibers. Canceling a fiber is possible by calling the `cancel` method on the `Fiber` instance. The following code snippet shows how:\n\n```scala 3\nimport in.rcard.yaes.Async.*\nimport java.util.concurrent.ConcurrentLinkedQueue\n\nval actualQueue = Async.run {\n  val queue = new ConcurrentLinkedQueue[String]()\n  val cancellable = Async.fork {\n    Async.delay(2.seconds)\n    queue.add(\"cancellable\")\n  }\n  val fb = Async.fork {\n    Async.delay(500.millis)\n    `cancellable.cancel()`\n    queue.add(\"fb2\")\n  }\n  cancellable.join()\n  queue\n}\n```\n\nCancellation is collaborative. In the above example, the fiber `cancellable` is marked for cancellation by the call `cancellable.cancel()`. However, the fiber is not immediately canceled. The fiber is canceled when it reaches the first operation that can be interrupted by the JVM. Hence, cancellation is based on the concept of interruption. In the above example, the `cancellable` is canceled when it reaches the `delay(2.seconds)` operation. The fiber will never be canceled if we remove the delay operation. A similar behavior is implemented by Kotlin coroutines (see [Kotlin Coroutines - A Comprehensive Introduction / Cancellation](https://rockthejvm.com/articles/kotlin-101-coroutines#cancellation) for further details).\n\nCancelling a fiber follows the relationship between parent and child jobs. If a parent's fiber is canceled, all the children's fibers are canceled as well:\n\n```scala 3\nimport in.rcard.yaes.Async.*\nimport java.util.concurrent.ConcurrentLinkedQueue\n\nval actualQueue = Async.run {\n  val queue = new ConcurrentLinkedQueue[String]()\n  val fb1 = Async.fork(\"fb1\") {\n    Async.fork(\"inner-fb\") {\n      Async.fork(\"inner-inner-fb\") {\n        Async.delay(6.seconds)\n        queue.add(\"inner-inner-fb\")\n      }\n\n      Async.delay(5.seconds)\n      queue.add(\"innerfb\")\n    }\n    Async.delay(1.second)\n    queue.add(\"fb1\")\n  }\n  Async.fork(\"fb2\") {\n    Async.delay(500.millis)\n    fb1.cancel()\n    queue.add(\"fb2\")\n  }\n  queue\n}\n```\n\nTrying to get the value from a canceled fiber will raise a `Cancelled` error. However, joining a canceled fiber will not raise any error.\n\n#### Structured Concurrency Primitives\n\nUsing the `Async.fork` DSL is quite low-level. The library provides a set of structured concurrency primitives that can be used to define more complex asynchronous computations. The available primitives are:\n\n- `Async.par`: Runs two asynchronous computations in parallel and returns both.\n- `Async.race`: Runs two asynchronous computations in parallel and returns the result of the first computation that finishes. The other one is canceled.\n- `Async.racePair`: Runs two asynchronous computations in parallel and returns the result of the first computation that finishes along with the fiber that is still running.\n- `Async.parTraverse`: Executes a function over all elements of a collection in parallel, returning results in input order.\n\n#### Parallel Traversal\n\nWhen you need to apply the same operation to every element of a collection in parallel, use `Async.parTraverse`. It forks one fiber per element, waits for all to finish, and returns the results **in the same order as the input**. If any computation fails, the remaining fibers are automatically cancelled (fail-fast):\n\n```scala 3\nimport in.rcard.yaes.Async.*\n\ncase class UserProfile(id: Int, name: String)\ndef fetchUserProfile(id: Int)(using Async): UserProfile = ???\n\nval profiles: Seq[UserProfile] = Async.run {\n  Async.parTraverse(List(1, 2, 3, 4, 5))(fetchUserProfile)\n}\n```\n\n`parTraverse` also composes seamlessly with other effects such as `Raise`:\n\n```scala 3\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\ndef validateAndFetch(id: Int)(using Async, Raise[String]): UserProfile =\n  if (id \u003c= 0) Raise.raise(s\"Invalid id: $id\")\n  else fetchUserProfile(id)\n\nval result: Either[String, Seq[UserProfile]] = Raise.either {\n  Async.run {\n    Async.parTraverse(List(1, 2, 3))(validateAndFetch)\n  }\n}\n```\n\n#### Graceful Shutdown Integration\n\nFor long-running applications and daemon processes, the `Async.withGracefulShutdown` handler provides automatic shutdown coordination with the `Shutdown` effect. This handler ensures your application can cleanly terminate concurrent operations within a specified deadline.\n\nWhen shutdown is initiated (either via JVM signals like SIGTERM/SIGINT or programmatically), the handler gives your main task a deadline to complete cleanup operations. If the deadline expires, remaining fibers are cooperatively cancelled and `Async.ShutdownTimedOut` is raised. This prevents hanging shutdowns while allowing in-flight work to complete gracefully.\n\n```scala\nimport in.rcard.yaes.{Async, Shutdown, Raise}\nimport in.rcard.yaes.Async.{Deadline, ShutdownTimedOut}\nimport scala.concurrent.duration.*\n\nval result: Either[ShutdownTimedOut, Unit] = Shutdown.run {\n  Raise.either {\n    Async.withGracefulShutdown(Deadline.after(30.seconds)) {\n      val serverFiber = Async.fork(\"server\") {\n        while (!Shutdown.isShuttingDown()) {\n          // Accept and process work\n        }\n\n        Shutdown.initiateShutdown()\n        // Cleanup after shutdown\n      }\n    }\n  }\n}\n```\n\nFor complete documentation including lifecycle details, deadline configuration, and practical examples, see [Async Effect - Graceful Shutdown](https://rcardin.github.io/yaes/effects/async.html#graceful-shutdown-with-async).\n\n### The `Raise` Effect\n\nThe `Raise[E]` type describes the possibility that a function can raise an error of type `E`. `E` can be a logic typed error or an exception. The DSL is heavily inspired by the [`raise4s`](https://github.com/rcardin/raise4s) library.\n\nLet's see an example:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\ndef divide(a: Int, b: Int)(using Raise[ArithmeticException]): Int =\n  if (b == 0) Raise.raise(new ArithmeticException(\"Division by zero\"))\n  else a / b\n```\n\nIn the above example, the `divide` function can raise an `ArithmeticException` if the second parameter is zero. In the example, we used an exception as the error type. However, we can use any type as the error type: \n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\nobject DivisionByZero\ntype DivisionByZero = DivisionByZero.type\n\ndef divide(a: Int, b: Int)(using Raise[DivisionByZero]): Int =\n  if (b == 0) Raise.raise(DivisionByZero)\n  else a / b\n```\n\nFor more concise syntax, you can use the `raises` infix type:\n\n```scala 3\nimport in.rcard.yaes.{Raise, raises}\n\n// Using the raises infix type\ndef divide(a: Int, b: Int): Int raises DivisionByZero =\n  if (b == 0) Raise.raise(DivisionByZero)\n  else a / b\n\n// Equivalent to using Raise[E] explicitly\ndef divideExplicit(a: Int, b: Int)(using Raise[DivisionByZero]): Int =\n  if (b == 0) Raise.raise(DivisionByZero)\n  else a / b\n```\n\nThe effect offers some functions to lift an program into an effectful computation that uses the `Raise[E]` effect. For example, we can rewrite the above example using the `ensure` utility function:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\ndef divide(a: Int, b: Int)(using Raise[DivisionByZero]): Int =\n  Raise.ensure(b != 0) { DivisionByZero }\n  a / b\n```\n\nIf we know that a function can throw an exception, we can catch it and trasform it into an error of type `E` with the `catching` function:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\ndef divide(a: Int, b: Int)(using Raise[DivisionByZero]): Int =\n  Raise.catching[ArithmeticException] {\n    a / b\n  } { _ =\u003e DivisionByZero }\n```\n\nThe effect defines many handlers to deal with the raised errors. For example, we can execute the effectful computation and handle the raised error as a union type:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\nval divisionByZeroResult: Int | DivisionByZero = Raise.run {\n    divide(10, 0)\n  }\n```\n\nAlternatively, we can handle the raised error transforming it into an `Either` type:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\nval divisionByZeroResult: Either[DivisionByZero, Int] = Raise.either {\n  divide(10, 0)\n}\n```\n\nIf we're not interested in propagating the exact reason of error, we can use the `option` handler. The `option` handler requires the block to raise `None` explicitly:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\ndef safeDivide(x: Int, y: Int)(using Raise[None.type]): Int =\n  if (y == 0) then Raise.raise(None)\n  else x / y\n\nval divisionByZeroResult: Option[Int] = Raise.option {\n  safeDivide(10, 0)\n}\n// divisionByZeroResult will be None\n```\n\nWe can even ignore the raised error returning a `Null` value. The `nullable` handler requires the block to raise `null` explicitly:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\ndef safeDivide(x: Int, y: Int)(using Raise[Null]): Int =\n  if (y == 0) then Raise.raise(null)\n  else x / y\n\nval divisionByZeroResult: Int | Null = Raise.nullable {\n  safeDivide(10, 0)\n}\n// divisionByZeroResult will be null\n```\n\n#### Error Mapping with `MapError`\n\nThe `Raise` effect provides a powerful `MapError` strategy that allows you to automatically map errors from one type to another using a `given` instance. This is particularly useful when you need to transform errors in a compositional way across different layers of your application.\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\n// Define different error types for different layers\nsealed trait DatabaseError\ncase object ConnectionTimeout extends DatabaseError\ncase object RecordNotFound extends DatabaseError\n\nsealed trait ServiceError\ncase class ValidationFailed(message: String) extends ServiceError\ncase class OperationFailed(cause: String) extends ServiceError\n\n// A function that raises DatabaseError\ndef findUserInDatabase(id: Int)(using Raise[DatabaseError]): User =\n  if (id \u003c 0) Raise.raise(RecordNotFound)\n  else User(s\"User$id\")\n\n// Use MapError to automatically transform DatabaseError to ServiceError\ndef findUser(id: Int)(using Raise[ServiceError]): User = {\n  // Define the mapping strategy as a given instance\n  given MapError[DatabaseError, ServiceError] = MapError {\n    case ConnectionTimeout =\u003e OperationFailed(\"Database unavailable\")\n    case RecordNotFound =\u003e ValidationFailed(\"User not found\")\n  }\n  \n  // The error will be automatically mapped from DatabaseError to ServiceError\n  findUserInDatabase(id)\n}\n\n// Usage\nval result: ServiceError | User = Raise.run {\n  findUser(-1)\n}\n// result will be ValidationFailed(\"User not found\")\n```\n\nThe `MapError` strategy is particularly useful when working with layered architectures where different layers define their own error types, allowing for clean separation of concerns while maintaining composability.\n\n#### Error Accumulation\n\nThe `Raise` effect allows you to accumulate multiple errors instead of short-circuiting on the first one using `accumulate` and `accumulating`:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\ndef validateName(name: String)(using Raise[String]): String =\n  if (name.nonEmpty) name else Raise.raise(\"Name cannot be empty\")\n\ndef validateAge(age: Int)(using Raise[String]): Int =\n  if (age \u003e= 0) age else Raise.raise(\"Age cannot be negative\")\n\nval result = Raise.either {\n  Raise.accumulate {\n    val name = accumulating { validateName(\"\") }\n    val age = accumulating { validateAge(-1) }\n    (name, age)\n  }\n}\n// result will be Left(List(\"Name cannot be empty\", \"Age cannot be negative\"))\n```\n\n⚠️ **Important**: When using the `accumulate` function with lists or other collections, you **must** assign the result to a variable before returning it. Direct return of accumulated collections may not work correctly.\n\n```scala 3\n// ✅ CORRECT - Assign to variable first\nval result = Raise.either {\n  Raise.accumulate {\n    val processedItems = List(1, 2, 3, 4, 5).map { i =\u003e\n      accumulating {\n        if (i % 2 == 0) Raise.raise(i.toString)\n        else i\n      }\n    }\n    processedItems  // Return the assigned variable\n  }\n}\n\n// ❌ INCORRECT - Direct return may not work\nval result = Raise.either {\n  Raise.accumulate {\n    List(1, 2, 3, 4, 5).map { i =\u003e\n      accumulating {\n        if (i % 2 == 0) Raise.raise(i.toString)\n        else i\n      }\n    }  // Direct return without assignment\n  }\n}\n```\n\nThe `mapAccumulating` function allows you to transform collections while accumulating any errors that occur during the transformation. This is useful when you want to process all elements and collect all errors rather than stopping at the first failure.\n\n**Simple Error Accumulation:**\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\ndef validateNumber(n: Int)(using Raise[String]): Int =\n  if (n \u003e 0) n else Raise.raise(s\"$n is not positive\")\n\n// Transform all elements, accumulating errors\nval result = Raise.either {\n  Raise.mapAccumulating(List(1, -2, 3, -4, 5)) { number =\u003e\n    validateNumber(number)\n  }\n}\n// result will be Left(List(\"-2 is not positive\", \"-4 is not positive\"))\n\n// With all valid inputs\nval successResult = Raise.either {\n  Raise.mapAccumulating(List(1, 2, 3, 4, 5)) { number =\u003e\n    validateNumber(number)\n  }\n}\n// successResult will be Right(List(1, 2, 3, 4, 5))\n```\n\n**Custom Error Combination:**\n\nFor more complex error types, you can provide a custom error combination function:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\ncase class ValidationErrors(errors: List[String])\n\ndef combineErrors(error1: ValidationErrors, error2: ValidationErrors): ValidationErrors =\n  ValidationErrors(error1.errors ++ error2.errors)\n\ndef validateUserData(data: String)(using Raise[ValidationErrors]): String =\n  if (data.isEmpty) Raise.raise(ValidationErrors(List(\"Data cannot be empty\")))\n  else if (data.length \u003c 3) Raise.raise(ValidationErrors(List(\"Data too short\")))\n  else data\n\nval result = Raise.either {\n  Raise.mapAccumulating(List(\"Alice\", \"\", \"Bo\", \"Charlie\"), combineErrors) { userData =\u003e\n    validateUserData(userData)\n  }\n}\n// result will be Left(ValidationErrors(List(\"Data cannot be empty\", \"Data too short\")))\n```\n\n#### Polymorphic Error Accumulation\n\nThe `accumulate` function is polymorphic and can collect errors into different collection types beyond `List`. This is particularly useful when you want to ensure at compile-time that at least one error occurred (using `NonEmptyList` or `NonEmptyChain` from Cats).\n\n**Using NonEmptyList** (requires `yaes-cats` module):\n\n```scala 3\nimport in.rcard.yaes.{Raise, RaiseNel}  // RaiseNel is an alias for Raise[NonEmptyList[E]]\nimport in.rcard.yaes.Raise.accumulating\nimport in.rcard.yaes.instances.accumulate.given  // Import collector instances\nimport cats.data.NonEmptyList\n\ndef validatePositive(n: Int)(using Raise[String]): Int =\n  if (n \u003e 0) n else Raise.raise(s\"$n is not positive\")\n\nval result: Either[NonEmptyList[String], (Int, Int)] = Raise.either {\n  Raise.accumulate[NonEmptyList, String, (Int, Int)] {\n    val a = accumulating { validatePositive(-1) }\n    val b = accumulating { validatePositive(-2) }\n    (a, b)\n  }\n}\n// result will be Left(NonEmptyList(\"-1 is not positive\", List(\"-2 is not positive\")))\n\n// Or using the RaiseNel type alias for cleaner signatures:\nval cleanerResult: RaiseNel[String] ?=\u003e (Int, Int) =\n  Raise.accumulate[NonEmptyList, String, (Int, Int)] {\n    val a = accumulating { validatePositive(-1) }\n    val b = accumulating { validatePositive(-2) }\n    (a, b)\n  }\n```\n\n**Using NonEmptyChain** (requires `yaes-cats` module):\n\n```scala 3\nimport in.rcard.yaes.RaiseNec  // RaiseNec is an alias for Raise[NonEmptyChain[E]]\nimport cats.data.NonEmptyChain\n\nval result: Either[NonEmptyChain[String], List[Int]] = Raise.either {\n  Raise.accumulate[NonEmptyChain, String, List[Int]] {\n    val numbers = List(1, -2, 3, -4, 5).map { n =\u003e\n      accumulating { validatePositive(n) }\n    }\n    numbers\n  }\n}\n// result will be Left(NonEmptyChain(\"-2 is not positive\", \"-4 is not positive\"))\n\n// Or using the RaiseNec type alias:\nval cleanerResult: RaiseNec[String] ?=\u003e List[Int] =\n  Raise.accumulate[NonEmptyChain, String, List[Int]] {\n    val numbers = List(1, -2, 3, -4, 5).map { n =\u003e\n      accumulating { validatePositive(n) }\n    }\n    numbers\n  }\n```\n\n**Using List** (default behavior):\n\n```scala 3\nval result: Either[List[String], (Int, Int)] = Raise.either {\n  Raise.accumulate[List, String, (Int, Int)] {\n    val a = accumulating { validatePositive(-1) }\n    val b = accumulating { validatePositive(-2) }\n    (a, b)\n  }\n}\n// result will be Left(List(\"-1 is not positive\", \"-2 is not positive\"))\n```\n\nThe type parameter `M[_]` specifies the collection type for errors. The system requires an `AccumulateCollector[M]` instance to convert the internal error list to type `M[Error]`. Built-in collectors are provided for `List`, and the `yaes-cats` module provides collectors for `NonEmptyList` and `NonEmptyChain`.\n\n**Type Aliases:** The `yaes-cats` module provides convenient type aliases:\n- `RaiseNel[E]` = `Raise[NonEmptyList[E]]`\n- `RaiseNec[E]` = `Raise[NonEmptyChain[E]]`\n\nThese aliases make function signatures cleaner and follow Cats library conventions.\n\n#### Error Tracing\n\nThe `traced` function adds debugging capabilities by capturing stack traces when errors occur:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\n\n// Define custom tracing behavior\ngiven TraceWith[String] = trace =\u003e {\n  println(s\"Error: ${trace.original}\")\n  trace.printStackTrace()\n}\n\ndef riskyOperation(value: Int)(using Raise[String]): Int =\n  if (value \u003c 0) Raise.raise(\"Negative value not allowed\")\n  else value * 2\n\nval result = Raise.either {\n  traced {\n    riskyOperation(-5)\n  }\n}\n// Prints error details and stack trace, then returns Left(\"Negative value not allowed\")\n```\n\nYou can also use the default tracing strategy:\n\n```scala 3\nimport in.rcard.yaes.Raise.*\nimport in.rcard.yaes.Raise.given  // Import default tracing\n\nval result = Raise.either {\n  traced {\n    Raise.raise(\"Something went wrong\")\n  }\n}\n// Automatically prints stack trace\n```\n\n**Note**: Tracing has performance implications since it creates full stack traces.\n\n### The `Resource` Effect\n\nThe `Resource` effect provides automatic resource management with guaranteed cleanup. It ensures that all acquired resources are properly released in LIFO (Last In, First Out) order, even when exceptions occur. This is particularly useful for managing files, database connections, network connections, and other resources that need explicit cleanup.\n\n```scala 3\nimport in.rcard.yaes.Resource.*\nimport java.io.{FileInputStream, FileOutputStream}\n\ndef copyFile(source: String, target: String)(using Resource): Unit = {\n  val input = Resource.acquire(new FileInputStream(source))\n  val output = Resource.acquire(new FileOutputStream(target))\n  \n  // Copy file contents\n  val buffer = new Array[Byte](1024)\n  var bytesRead = input.read(buffer)\n  while (bytesRead != -1) {\n    output.write(buffer, 0, bytesRead)\n    bytesRead = input.read(buffer)\n  }\n}\n```\n\nThe `Resource` effect provides several methods for resource management:\n\n- `Resource.acquire`: For resources that implement `Closeable`, automatically calling `close()` when the scope ends\n- `Resource.install`: For custom resource management with explicit acquisition and release functions\n- `Resource.ensuring`: For registering cleanup actions that don't involve specific resources\n\nHere's an example using custom resource management:\n\n```scala 3\nimport in.rcard.yaes.Resource.*\n\ndef processWithConnection()(using Resource): String = {\n  val connection = Resource.install(openDatabaseConnection()) { conn =\u003e\n    conn.close()\n    println(\"Database connection closed\")\n  }\n  \n  Resource.ensuring {\n    println(\"Processing completed\")\n  }\n  \n  // Use connection safely\n  connection.executeQuery(\"SELECT * FROM users\")\n}\n```\n\nTo execute resource-managed code, use the `Resource.run` handler:\n\n```scala 3\nimport in.rcard.yaes.Resource.*\n\nval result = Resource.run {\n  copyFile(\"source.txt\", \"target.txt\")\n  processWithConnection()\n}\n// All resources are automatically cleaned up here, even if exceptions occurred\n```\n\nThe `Resource` effect guarantees that:\n- Resources are cleaned up in reverse order of acquisition (LIFO)\n- Cleanup occurs even if exceptions are thrown\n- Resource cleanup exceptions are handled appropriately\n- Original exceptions from the main program are preserved\n\n### The `Shutdown` Effect\n\nThe `Shutdown` effect provides graceful shutdown coordination for long-running applications. It automatically handles JVM shutdown signals (SIGTERM, SIGINT, Ctrl+C) and provides mechanisms to cleanly terminate concurrent operations while rejecting new work.\n\n```scala 3\nimport in.rcard.yaes.Shutdown.*\n\ndef processWork()(using Shutdown): Unit = {\n  while (!Shutdown.isShuttingDown()) {\n    // Process work items\n    println(\"Processing...\")\n    Thread.sleep(1000)\n  }\n  println(\"Shutdown initiated, stopping work\")\n}\n```\n\nThe `Shutdown` effect provides three main operations:\n\n- `Shutdown.isShuttingDown()`: Checks if shutdown has been initiated\n- `Shutdown.initiateShutdown()`: Manually triggers graceful shutdown\n- `Shutdown.onShutdown(hook)`: Registers callbacks to execute when shutdown begins\n\nHere's an example using shutdown hooks:\n\n```scala 3\nimport in.rcard.yaes.Shutdown.*\nimport in.rcard.yaes.Output.*\n\ndef serverWithHooks()(using Shutdown, Output): Unit = {\n  Shutdown.onShutdown(() =\u003e {\n    Output.printLn(\"Shutdown signal received\")\n    Output.printLn(\"Stopping new request acceptance\")\n  })\n\n  while (!Shutdown.isShuttingDown()) {\n    // Accept and process requests\n  }\n}\n```\n\nTo run shutdown-aware code, use the `Shutdown.run` handler:\n\n```scala 3\nimport in.rcard.yaes.Shutdown.*\nimport in.rcard.yaes.Output.*\n\nShutdown.run {\n  Output.run {\n    serverWithHooks()\n  }\n}\n// Automatically responds to Ctrl+C, SIGTERM, and container stop signals\n```\n\nThe `Shutdown` effect is particularly useful when combined with the `Async` effect for daemon processes and long-running services. It ensures graceful termination by:\n\n- Automatically registering JVM shutdown hooks\n- Providing state checks to reject new work during shutdown\n- Executing registered callbacks in order when shutdown is initiated\n- Being idempotent - multiple shutdown calls are safe\n- Wrapping hooks in exception handling so one failure doesn't prevent others\n\n### The `Input` Effect\n\nEvery time we need to read input from the console, we can use the `Input` effect. The `Input` effect provides a set of operations to read input from the console. Since the project is still in an experimental stage, the only one developed operation is the `readLn` function that reads a line from the console:\n\n```scala 3\nimport in.rcard.yaes.Input.*\nimport in.rcard.yaes.Raise.*\nimport java.io.IOException\n\nval name: (Input, Raise[IOException]) ?=\u003e String = Input.readLn()\n```\n\nThe effect uses the Scala `scala.io.StdIn` object under the hood, which uses the Java `System.in` object to read input from the console. Reading from the console can result in an `IOException`, so the `Input` effect requires a `Raise[IOException]` effect.\n\nTo run the effectful computation, we can use the provided handlers, which returns the read line:\n\n```scala 3\nimport in.rcard.yaes.Input.*\nimport in.rcard.yaes.Raise.*\nimport java.io.IOException\n\nval result: Either[IOException, String] = Raise.either {\n  Input.run {\n    name\n  }\n}\n```\n\nIn the above example, we use `Raise.either` to handle any `IOException` that may be thrown by the `Input` effect, returning an `Either[IOException, String]` that wraps the result or the exception.\n\n### The `Output` Effect\n\nThe `Output` effect provides a set of operations to print output to the console. Is uses the `scala.Console` object under the hood.\n\n```scala 3\nimport in.rcard.yaes.Output.*\n\nval program: Output ?=\u003e Unit = Output.printLn(\"Hello, world!\")\n```\n\nAs we can see, outputting to the console doesn't raise any error. The behavior mimics exactly the one exposed by the `scala.Console`, which silently ignores any error that can occur during the output operation.\n\nTo run the effectful computation, we can use the provided handlers:\n\n```scala 3\nimport in.rcard.yaes.Output.*\n\n// Prints \"Hello, world!\" to the console\nOutput.run {\n  program\n}\n```\n\nIn a similar way, we can output to system err using the `printErr` function:\n\n```scala 3\nimport in.rcard.yaes.Output.*\n\nval program: Output ?=\u003e Unit = Output.printErr(\"Hello, world!\")\n```\n\n### The `Random` Effect\n\nThe `Random` effect provides a set of operations to generate random content. If we need to generate non-deterministic content, we can use it. Under the hood, the effect uses the `scala.util.Random` object. As we saw in the introduction, we can use the `Random` effect to define a function that generates a random boolean:\n\n```scala 3\nimport in.rcard.yaes.Random.*\n\ndef flipCoin(using Random): Boolean = Random.nextBoolean\n```\n\nThe other random content we can generate is:\n\n- `nextInt`: Generates a random integer.\n- `nextDouble`: Generates a random double.\n- `nextLong`: Generates a random long.\n- `nextUuid`: Generates an RFC 4122 version 4 UUID as a lowercase `String`. Derived from two `nextLong` calls so the handler fully controls the result.\n\nAs usual, we can run the effectful computation using the provided handlers:\n\n```scala 3\nimport in.rcard.yaes.Random.*\n\nval result: Boolean = Random.run {\n  flipCoin\n}\n```\n\n### The `Clock` Effect\n\nThe `Clock` effect provides a set of operations to manage time effectfully. It's possible to get the current time, even in a monotonic way. The `Clock.now` function returns a `java.time.Instant`, while the `Clock.nowMonotonic` returns a strictly monotonically increasing time value, guaranteed to always move forward. Returns a `Duration` rather than an Instant because monotonic time represents the time elapsed since some arbitrary starting point, not a specific point in calendar time.\n\nBoth functions use the `java.time` package under the hood.\n\n```scala 3\nimport in.rcard.yaes.Clock.*\nimport in.rcard.yaes.Output.*\n\nval program = Output.run {\n  Clock.run {\n    val now = Clock.now()\n    val nowMonotonic = Clock.nowMonotonic()\n    Output.printLn(s\"Now: $now\")\n    Output.printLn(s\"Now monotonic: $nowMonotonic\")\n  }\n}\n```\n\n### The `System` Effect\n\nThe `System` effect provides a set of operations to manage system properties and environment variables. It allows for reading system properties and environment variables in a type-safe way.\n\nUse the `System.env` function to read an environment variable and eventually use a default value if the variable is not set:\n\n```scala 3\nimport in.rcard.yaes.System.*\nimport in.rcard.yaes.Raise.*\n\nval port: (System, Raise[NumberFormatException]) ?=\u003e Option[Int] = System.env[Int](\"PORT\")\nval host: System ?=\u003e String = System.env[String](\"HOST\", \"localhost\")\n```\n\nThe same applies to system properties. Use the `System.property` function to read a system property and eventually use a default value if the property is not set:\n\n```scala 3\nimport in.rcard.yaes.System.*\nimport in.rcard.yaes.Raise.*\n\nval port: (System, Raise[NumberFormatException]) ?=\u003e Option[Int] = System.property[Int](\"server.port\")\nval host: System ?=\u003e String = System.property[String](\"server.host\", \"localhost\")\n```\n\nThe available types for properties and environment variables are:\n\n- `String`: A string value.\n- `Int`: An integer value.\n- `Long`: A long value.\n- `Double`: A double value.\n- `Boolean`: A boolean value.\n- `Float`: A float value.\n- `Short`: A short value.\n- `Byte`: A byte value.\n- `Char`: A char value.\n\n### The `State` Effect\n\nThe `State[S]` effect enables stateful computations in a purely functional manner. It provides operations to get, set, update, and use state values within a controlled scope, allowing you to work with mutable state without compromising functional programming principles.\n\nThe `State` effect manages a single piece of state of type `S` throughout a computation. All state operations are performed within the context of a `State.run` block, which provides isolation and ensures the state is properly managed.\n\n**Note:** The State effect is not thread-safe. Use appropriate synchronization mechanisms when accessing state from multiple threads.\n\n#### Basic Usage\n\n```scala 3\nimport in.rcard.yaes.State.*\n\nval (finalState, result) = State.run(0) {\n  val current = State.get[Int]\n  State.set(current + 5)\n  State.get[Int]\n}\n// finalState = 5, result = 5\n```\n\n#### Updating State\n\n```scala 3\nimport in.rcard.yaes.State.*\n\nval (finalState, result) = State.run(10) {\n  val doubled = State.update[Int](_ * 2)\n  val tripled = State.update[Int](_ * 3)\n  tripled\n}\n// finalState = 60, result = 60\n```\n\n#### Using State Without Modification\n\n```scala 3\nimport in.rcard.yaes.State.*\n\ncase class User(name: String, age: Int)\n\nval (finalState, nameLength) = State.run(User(\"Alice\", 30)) {\n  State.use[User, Int](_.name.length)\n}\n// finalState = User(\"Alice\", 30), nameLength = 5\n```\n\n#### Combining with Other Effects\n\n```scala 3\nimport in.rcard.yaes.State.*\nimport in.rcard.yaes.Random.*\n\ndef randomWalk(steps: Int)(using State[Int], Random): Int = {\n  if (steps \u003c= 0) State.get[Int]\n  else {\n    val direction = if (Random.nextBoolean) 1 else -1\n    State.update(_ + direction)\n    randomWalk(steps - 1)\n  }\n}\n\nval (finalPosition, result) = State.run(0) {\n  Random.run {\n    randomWalk(10)\n  }\n}\n```\n\nThe `State` effect provides the following operations:\n- `State.get[S]`: Retrieves the current state value without modifying it\n- `State.set[S](value: S)`: Sets the state to a new value and returns the previous state value\n- `State.update[S](f: S =\u003e S)`: Updates the state using a transformation function and returns the new state value\n- `State.use[S, A](f: S =\u003e A)`: Applies a function to the current state and returns the result without modifying the state\n- `State.run[S, A](initialState: S)(block: State[S] ?=\u003e A)`: Runs a stateful computation with an initial state value, returning both the final state and the computation result\n\n### The `Writer` Effect\n\nThe `Writer[W]` effect enables pure, append-only value accumulation during computations. Values are collected into a `Vector[W]` and returned alongside the computation result as a tuple `(Vector[W], A)`. This is useful for collecting logs, events, metrics, or any other data during a computation.\n\n**Note:** The Writer effect is not thread-safe. Use appropriate synchronization mechanisms when accessing from multiple threads.\n\n#### Basic Usage\n\n```scala 3\nimport in.rcard.yaes.Writer.*\n\nval (log, result) = Writer.run[String, Int] {\n  Writer.write(\"starting\")\n  Writer.write(\"computing\")\n  42\n}\n// log = Vector(\"starting\", \"computing\"), result = 42\n```\n\nFor more concise syntax, you can use the `writes` infix type:\n\n```scala 3\nimport in.rcard.yaes.{Writer, writes}\n\ndef computation: Int writes String = {\n  Writer.write(\"log entry\")\n  42\n}\n```\n\n#### Writing Multiple Values\n\nUse `writeAll` to append multiple values at once:\n\n```scala 3\nimport in.rcard.yaes.Writer.*\n\nval (log, _) = Writer.run[Int, Unit] {\n  Writer.write(1)\n  Writer.writeAll(List(2, 3, 4))\n  Writer.write(5)\n}\n// log = Vector(1, 2, 3, 4, 5)\n```\n\n#### Capturing Writes\n\nThe `capture` operation records writes from a block, returning them alongside the block's result. Writes are also forwarded to the outer scope:\n\n```scala 3\nimport in.rcard.yaes.Writer.*\n\nval (outerLog, (innerLog, result)) = Writer.run[String, (Vector[String], Int)] {\n  Writer.write(\"before\")\n  val captured = Writer.capture[String, Int] {\n    Writer.write(\"inside\")\n    99\n  }\n  Writer.write(\"after\")\n  captured\n}\n// outerLog = Vector(\"before\", \"inside\", \"after\")\n// innerLog = Vector(\"inside\"), result = 99\n```\n\n#### Combining with Other Effects\n\n```scala 3\nimport in.rcard.yaes.Writer.*\nimport in.rcard.yaes.State.*\n\nval (state, (log, result)) = State.run(0) {\n  Writer.run[String, Int] {\n    Writer.write(\"start\")\n    State.update[Int](_ + 1)\n    Writer.write(s\"count=${State.get[Int]}\")\n    State.get[Int]\n  }\n}\n// state = 1, log = Vector(\"start\", \"count=1\"), result = 1\n```\n\nThe `Writer` effect provides the following operations:\n- `Writer.write[W](w: W)`: Appends a single value to the accumulated output\n- `Writer.writeAll[W](ws: IterableOnce[W])`: Appends multiple values at once\n- `Writer.capture[W, A](block: Writer[W] ?=\u003e A)`: Captures writes from a block, forwarding them to the outer scope, and returns `(Vector[W], A)`\n- `Writer.run[W, A](block: Writer[W] ?=\u003e A)`: Runs a computation with the Writer effect, returning `(Vector[W], A)`\n\n### The `Reader` Effect\n\nThe `Reader[R]` effect provides read-only access to an environment value of type `R`. It allows computations to read a shared value and temporarily override it via `local`, completing the classic Reader/Writer/State trio.\n\nThe environment value is immutable within a scope. The `local` operation creates a fresh scope with a modified value, restoring the original after the block completes. This makes `Reader` thread-safe by construction.\n\n#### Basic Usage\n\n```scala 3\nimport in.rcard.yaes.Reader\n\ncase class Config(maxRetries: Int, timeout: Int)\n\nval result = Reader.run(Config(3, 5000)) {\n  Reader.read[Config].maxRetries\n}\n// result = 3\n```\n\nFor more concise syntax, you can use the `reads` infix type:\n\n```scala 3\nimport in.rcard.yaes.{Reader, reads}\n\ndef getRetries: Int reads Config =\n  Reader.read[Config].maxRetries\n```\n\n#### Local Overrides\n\nUse `local` to temporarily modify the environment value for a block:\n\n```scala 3\nimport in.rcard.yaes.Reader\n\ncase class Config(maxRetries: Int, timeout: Int)\n\nval result = Reader.run(Config(3, 5000)) {\n  val before = Reader.read[Config].maxRetries        // 3\n  val during = Reader.local(_.copy(maxRetries = 10)) {\n    Reader.read[Config].maxRetries                    // 10\n  }\n  val after = Reader.read[Config].maxRetries          // 3 (restored)\n  (before, during, after)\n}\n// result = (3, 10, 3)\n```\n\n#### Combining with Other Effects\n\n```scala 3\nimport in.rcard.yaes.{Raise, Reader, raises, reads}\n\ncase class Config(maxRetries: Int)\n\ndef validate(value: Int): Unit raises String reads Config = {\n  val max = Reader.read[Config].maxRetries\n  if (value \u003e max) Raise.raise(s\"$value exceeds max $max\")\n}\n\nval result = Reader.run(Config(5)) {\n  Raise.either[String, Unit] {\n    validate(10)\n  }\n}\n// result = Left(\"10 exceeds max 5\")\n```\n\nThe `Reader` effect provides the following operations:\n- `Reader.read[R]`: Returns the current environment value\n- `Reader.local[R, A](f: R =\u003e R)(block: Reader[R] ?=\u003e A)`: Runs a block with a modified environment of the same type, restoring the original after\n- `Reader.local[R1, R2, A](f: R1 =\u003e R2)(block: Reader[R2] ?=\u003e A)`: Runs a block with a transformed environment value (possibly changing its type), restoring the original after\n- `Reader.reader[R](value: R)`: Creates a `Reader[R]` capability from a concrete environment value; use this when you need to pass or provide the `Reader[R]` instance explicitly\n- `Reader.run[R, A](value: R)(block: Reader[R] ?=\u003e A)`: Runs a computation with the Reader effect, returning `A` directly; use this as the convenient entry point when you just want to execute a block that requires `Reader[R]`\n\n### The `Log` Effect\n\nThe `Log` effect provides the capability to log messages at different levels. The available levels are:\n  - `TRACE`\n  - `DEBUG`\n  - `INFO`\n  - `WARN`\n  - `ERROR`\n  - `FATAL`\n  \nWe can log using a concrete implementation of the `in.rcard.yaes.Logger` interface. Each logger instance has a name. To create a logger, we can use the `Log.getLogger` method:\n\n```scala 3\nimport in.rcard.yaes.Log.*\n\nval logger: Log ?=\u003e Logger = Log.getLogger(\"TestLogger\")\n```\n\nIn `yaes-core`, the default logger implementation is the `ConsoleLogger`, which logs messages to the console. The message printed to the console has the following format:\n\n```\n2025-04-22T19:55:59 - TRACE - TestLogger - Trace message\n```\n\nTo run the effectful computation, we can use the provided handlers. The `Log.run` method accepts a `level` parameter that controls the minimum severity of messages that will be emitted. The default level is `Log.Level.Debug`:\n\n```scala 3\nimport in.rcard.yaes.Log.*\n\nval program = Log.run(Log.Level.Info) {\n  val logger = Log.getLogger(\"TestLogger\")\n\n  logger.debug(\"This will NOT be printed\")\n  logger.info(\"Info message\")\n}\n```\n\nIt's possible to change the clock used by the logger. By default, the `java.time.Clock.systemDefaultZone()` is used. The clock is provided as a given parameter to the `Log.run` handler method. The default clock is defined as a given instance in the `Log` object.\n\n```scala 3\nobject Log {\n  given defaultClock: java.time.Clock = java.time.Clock.systemDefaultZone()\n  // ...\n}\n```\n\n#### SLF4J Integration\n\nThe `yaes-slf4j` module provides an alternative handler that delegates logging to any SLF4J-compatible backend (Logback, Log4j2, etc.). Simply replace `Log.run` with `Slf4jLog.run` — all existing application code remains unchanged:\n\n```scala 3\nimport in.rcard.yaes.Log\nimport in.rcard.yaes.slf4j.Slf4jLog\n\nSlf4jLog.run {\n  val logger = Log.getLogger(\"MyService\")\n  logger.info(\"Hello from SLF4J!\")\n}\n```\n\nLevel filtering is controlled by the SLF4J backend configuration instead of a handler parameter. See the [yaes-slf4j README](yaes-slf4j/README.md) for full details.\n\n### The Retry Handler\n\nThe `Retry` handler re-executes a failing block according to a `Schedule` retry policy. It catches typed errors via `Raise[E]` and uses `Async` for delays between attempts.\n\n\u003e **Note:** `Retry` is not an effect — it orchestrates existing effects (`Raise` and `Async`). The block being retried just runs, succeeds, or fails.\n\n#### Schedule Policies\n\nA `Schedule` computes the delay for each retry attempt. Schedules compose via chaining. The `jitter` extension requires the `Random` effect in scope:\n\n```scala 3\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\nimport scala.concurrent.duration.*\n\n// Fixed delay\nval fixed = Schedule.fixed(500.millis)\n\n// Exponential backoff with cap\nval exponential = Schedule.exponential(100.millis, factor = 2.0, max = 30.seconds)\n\n// Compose: exponential + jitter + max attempts\n// jitter requires Random in scope; Random.run wraps the entire usage context\nval composed: Either[DbError, String] = Random.run {\n  Async.run {\n    Raise.either {\n      Retry[DbError](\n        Schedule\n          .exponential(100.millis, factor = 2.0, max = 30.seconds)\n          .jitter(0.25)\n          .attempts(5)\n      ) {\n        findUser(42)\n      }\n    }\n  }\n}\n```\n\n#### Using Retry\n\n```scala 3\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\nimport scala.concurrent.duration.*\n\ncase class DbError(msg: String)\n\ndef findUser(id: Int)(using Raise[DbError]): String =\n  Raise.raise(DbError(\"connection timeout\"))\n\nval result: Either[DbError, String] = Async.run {\n  Raise.either {\n    Retry[DbError](Schedule.fixed(500.millis).attempts(3)) {\n      findUser(42)\n    }\n  }\n}\n// result will be Left(DbError(\"connection timeout\")) after 3 total attempts\n```\n\nIf the block succeeds on any attempt, its value is returned immediately. If all attempts are exhausted, the last error is re-raised via the outer `Raise[E]`. Only errors of the specified type `E` trigger retries — other error types propagate immediately.\n\n## Communication Primitives\n\nBeyond effects, λÆS provides communication primitives for coordinating between asynchronous computations.\n\n### Channels\n\nA `Channel` is a communication primitive for transferring data between asynchronous computations (fibers). Conceptually, a channel is similar to `java.util.concurrent.BlockingQueue`, but it has suspending operations instead of blocking ones and can be closed.\n\nChannels are particularly useful when you need to:\n- Share data between multiple fibers\n- Implement producer-consumer patterns\n- Create pipelines of asynchronous transformations\n- Coordinate work between concurrent computations\n\n#### Channel Types\n\nChannels support different buffer configurations that control how elements are buffered and when senders/receivers suspend:\n\n**Unbounded Channel**: A channel with unlimited buffer capacity that never suspends the sender.\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\nval channel = Channel.unbounded[Int]()\n\nRaise.run {\n  Async.run {\n    Async.fork {\n      // These sends will never suspend\n      channel.send(1)\n      channel.send(2)\n      channel.send(3)\n    }\n  }\n}\n```\n\n**Bounded Channel**: A channel with a fixed buffer capacity. When the buffer is full, behavior depends on the overflow policy (default is to suspend the sender).\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Channel.OverflowStrategy\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\n// Default: suspend when full\nval channel1 = Channel.bounded[Int](capacity = 2)\n\n// Drop oldest element when full\nval channel2 = Channel.bounded[Int](capacity = 2, onOverflow = OverflowStrategy.DROP_OLDEST)\n\n// Drop newest element when full\nval channel3 = Channel.bounded[Int](capacity = 2, onOverflow = OverflowStrategy.DROP_LATEST)\n\nRaise.run {\n  Async.run {\n    Async.fork {\n      channel1.send(1) // Succeeds immediately\n      channel1.send(2) // Succeeds immediately\n      channel1.send(3) // Suspends until receiver takes an element\n    }\n  }\n}\n```\n\n**Buffer Overflow Policies**: Bounded channels support different strategies for handling buffer overflow:\n\n- `OverflowStrategy.SUSPEND` (default): The sender suspends until space becomes available, providing backpressure\n- `OverflowStrategy.DROP_OLDEST`: The oldest element in the buffer is dropped to make space for the new element\n- `OverflowStrategy.DROP_LATEST`: The new element is discarded and the buffer remains unchanged\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Channel.OverflowStrategy\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\n// Channel that never suspends, dropping old elements when full\nval channel = Channel.bounded[Int](capacity = 3, onOverflow = OverflowStrategy.DROP_OLDEST)\n\nRaise.run {\n  Async.run {\n    Async.fork {\n      (1 to 5).foreach(channel.send) // Sends never suspend\n      channel.close()\n    }\n    \n    channel.foreach(println) // Prints: 3, 4, 5 (first two were dropped)\n  }\n}\n```\n\n**Rendezvous Channel**: A channel with no buffer. The sender and receiver must meet (rendezvous): `send` suspends until another computation invokes `receive`, and vice versa.\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\nval channel = Channel.rendezvous[String]()\n\nRaise.run {\n  Async.run {\n    val sender = Async.fork {\n      channel.send(\"hello\") // Suspends until receiver is ready\n      println(\"Message sent\")\n    }\n\n    val receiver = Async.fork {\n      val msg = channel.receive() // Suspends until sender is ready\n      println(s\"Received: $msg\")\n    }\n  }\n}\n```\n\n#### Basic Operations\n\nChannels are composed of two interfaces:\n- **`SendChannel`**: For sending elements (can also close the channel)\n- **`ReceiveChannel`**: For receiving elements (can also cancel the channel)\n\n\u003e **Note:** As of version 0.11.0, channel operations (`send`, `receive`, `cancel`, `foreach`) no longer require an `Async` context - they work with just a `Raise` context. This makes channels more flexible and accurately reflects their implementation using JVM synchronization primitives. Builder functions like `produce`, `channelFlow`, and `Flow.buffer` still require `Async` as they use structured concurrency.\n\n**Sending and Receiving**:\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\nval channel = Channel.unbounded[Int]()\n\nRaise.run {\n  Async.run {\n    // Producer fiber\n    Async.fork {\n      channel.send(1)\n      channel.send(2)\n      channel.send(3)\n      channel.close() // Signal no more elements\n    }\n\n    // Consumer fiber\n    channel.foreach { value =\u003e\n      println(s\"Received: $value\")\n    }\n    // Prints: Received: 1, Received: 2, Received: 3\n  }\n}\n```\n\n**Closing vs Canceling**:\n\n- **`close()`**: Prevents further sends but allows receiving remaining buffered elements\n- **`cancel()`**: Immediately clears all buffered elements and marks the channel as cancelled\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\nval channel = Channel.unbounded[Int]()\n\nRaise.run {\n  Async.run {\n    Async.fork {\n      channel.send(1)\n      channel.send(2)\n    }\n    channel.close() // No more sends allowed\n    \n    println(channel.receive()) // Prints: 1\n    println(channel.receive()) // Prints: 2\n    // channel.receive() // Would raise ChannelClosed\n  }\n}\n```\n\n#### Producer DSL\n\nThe `produce` and `produceWith` functions provide a convenient DSL for creating channels with producer coroutines:\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Channel.Producer\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\nRaise.run {\n  Async.run {\n    // Create an unbounded channel with a producer\n    val channel = Channel.produce[Int] {\n      (1 to 10).foreach { i =\u003e\n        Producer.send(i * i)\n      }\n      // Channel automatically closed when block completes\n    }\n\n    // Consume the produced elements\n    channel.foreach { value =\u003e\n      println(s\"Square: $value\")\n    }\n  }\n}\n```\n\nYou can also specify the channel type with `produceWith`:\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Channel.Producer\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\nRaise.run {\n  Async.run {\n    // Create a bounded producer with backpressure\n    val channel = Channel.produceWith(Channel.Type.Bounded(5)) {\n      var count = 0\n      while (count \u003c 100) {\n        Producer.send(count)\n        count += 1\n      }\n    }\n\n    // Consume with backpressure\n    channel.foreach { value =\u003e\n      Async.delay(100.millis) // Slow consumer\n      println(value)\n    }\n  }\n}\n```\n\n#### Channel Flow Builder\n\nThe `channelFlow` and `channelFlowWith` functions provide a bridge between channels and flows, creating cold flows where elements are emitted through a `Producer` context. Unlike `produce`, which returns a `ReceiveChannel`, `channelFlow` returns a `Flow` that can be composed with flow operators.\n\n**Basic usage with `channelFlow`:**\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Async.*\n\nval flow = Channel.channelFlow[Int] {\n  Channel.Producer.send(1)\n  Channel.Producer.send(2)\n  Channel.Producer.send(3)\n}\n\nval result = scala.collection.mutable.ArrayBuffer[Int]()\nflow.collect { value =\u003e result += value }\n// result contains: 1, 2, 3\n```\n\n**With custom channel type using `channelFlowWith`:**\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\nval flow = Channel.channelFlowWith[Int](Channel.Type.Bounded(5)) {\n  (1 to 100).foreach(Channel.Producer.send)\n}\n\nval result = scala.collection.mutable.ArrayBuffer[Int]()\nflow.collect { value =\u003e result += value }\n// result contains: 1 to 100\n```\n\n**Concurrent emission from multiple fibers:**\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\nval flow = Channel.channelFlow[Int] {\n  Async.fork {\n    Channel.Producer.send(1)\n    Channel.Producer.send(2)\n  }\n\n  Async.fork {\n    Channel.Producer.send(3)\n    Channel.Producer.send(4)\n  }\n}\n\n\nval result = scala.collection.mutable.ArrayBuffer[Int]()\nflow.collect { value =\u003e result += value }\n// result contains all four values\n```\n\n**Merging multiple flows:**\n\n```scala 3\nimport in.rcard.yaes.{Channel, Flow}\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\ndef merge[T](flow1: Flow[T], flow2: Flow[T]): Flow[T] =\n  Channel.channelFlow[T] {\n    Async.fork {\n      flow1.collect { value =\u003e Channel.Producer.send(value) }\n    }\n\n    Async.fork {\n      flow2.collect { value =\u003e Channel.Producer.send(value) }\n    }\n  }\n\nval flow1 = Flow(1, 2, 3)\nval flow2 = Flow(4, 5, 6)\n\nval result = scala.collection.mutable.ArrayBuffer[Int]()\nmerge(flow1, flow2).collect { value =\u003e result += value }\n// result contains all six values\n```\n\nKey features:\n- **Cold execution**: The builder block executes every time `collect` is called on the flow\n- **Concurrent emission**: Supports multiple fibers sending to the same producer\n- **Flow composition**: Returns a `Flow` that can be used with all flow operators (map, filter, take, etc.)\n\n**Design Decision: Internal vs External `Async` Context**\n\nYou might notice that `channelFlow` doesn't require an external `Async` effect to run, unlike combinators such as `par` and `race`. This is intentional:\n\n| Category | Examples | `Async` Required | Reason |\n|----------|----------|------------------|--------|\n| **Combinators** | `par`, `race`, `zipWith` | Yes (external) | Compose existing computations; caller controls concurrency scope |\n| **Builders** | `channelFlow`, `Flow.flow` | No (internal) | Encapsulate their own effects; `Async.run` is part of `collect` implementation |\n\nThis design ensures that `channelFlow` produces a standard `Flow[T]` that can be used anywhere a `Flow` is expected, without leaking concurrency requirements to callers. The `Async.run` is invoked internally when `collect` is called, making each collection trigger a fresh concurrent computation.\n\n#### Flow Buffering\n\nThe `buffer` operator allows flow emissions to be buffered via a channel, enabling the producer (upstream flow) and consumer (downstream collector) to run concurrently. This can improve performance when emissions and collection have different speeds.\n\n**Basic usage with unbounded buffer (default):**\n\n```scala 3\nimport in.rcard.yaes.{Channel, Flow}\nimport in.rcard.yaes.Channel.buffer\n\nval flow = Flow(1, 2, 3, 4, 5)\n\nval result = scala.collection.mutable.ArrayBuffer[Int]()\nflow.buffer().collect { value =\u003e result += value }\n// result contains: 1, 2, 3, 4, 5\n```\n\n**With bounded buffer for backpressure:**\n\n```scala 3\nimport in.rcard.yaes.{Channel, Flow}\nimport in.rcard.yaes.Channel.buffer\n\nval flow = Flow(1, 2, 3, 4, 5)\n\nval result = scala.collection.mutable.ArrayBuffer[Int]()\nflow.buffer(Channel.Type.Bounded(2)).collect { value =\u003e result += value }\n// result contains: 1, 2, 3, 4, 5\n```\n\n**With overflow strategies:**\n\n```scala 3\nimport in.rcard.yaes.{Channel, Flow}\nimport in.rcard.yaes.Channel.{buffer, OverflowStrategy}\nimport in.rcard.yaes.Async.*\nimport scala.concurrent.duration.*\n\n// DROP_OLDEST: drops oldest buffered values when full\nAsync.run {\n  val flow1 = Flow(1, 2, 3, 4, 5)\n  flow1.buffer(Channel.Type.Bounded(2, OverflowStrategy.DROP_OLDEST)).collect { value =\u003e\n    Async.delay(100.millis) // Slow consumer\n    println(value)\n  }\n}\n// May print: 1, 4, 5 (oldest values dropped)\n\n// DROP_LATEST: drops new values when buffer is full\nAsync.run {\n  val flow2 = Flow(1, 2, 3, 4, 5)\n  flow2.buffer(Channel.Type.Bounded(2, OverflowStrategy.DROP_LATEST)).collect { value =\u003e\n    Async.delay(100.millis) // Slow consumer\n    println(value)\n  }\n}\n// May print: 1, 2, 3 (latest values dropped)\n```\n\nKey features:\n- **Cold operator**: The producer doesn't start until `collect` is called\n- **Concurrent execution**: Producer and consumer run in separate fibers\n- **Configurable buffering**: Supports unbounded, bounded, and rendezvous channels\n- **Overflow strategies**: SUSPEND (default), DROP_OLDEST, or DROP_LATEST for bounded channels\n\n#### Reactive Streams Integration\n\nλÆS provides seamless integration with Java Reactive Streams through the `FlowPublisher` class. This allows you to convert YAES Flows into standard `java.util.concurrent.Flow.Publisher` instances that can be consumed by any Reactive Streams-compliant library.\n\n**Key Benefits:**\n- **Interoperability**: Integrate with reactive frameworks like Akka Streams, Project Reactor, and RxJava\n- **Backpressure**: Leverage Reactive Streams demand management and backpressure protocol\n- **Spec Compliance**: Fully compliant with the Reactive Streams specification\n- **Type-Safe**: Maintains type safety throughout the conversion\n\n**Basic Usage:**\n\n```scala 3\nimport in.rcard.yaes.{Flow, FlowPublisher}\nimport in.rcard.yaes.FlowPublisher.asPublisher\nimport in.rcard.yaes.Async.*\nimport java.util.concurrent.Flow.{Subscriber, Subscription}\n\nval flow = Flow(1, 2, 3, 4, 5)\n\nAsync.run {\n  val publisher = flow.asPublisher()\n\n  publisher.subscribe(new Subscriber[Int] {\n    var subscription: Subscription = _\n\n    override def onSubscribe(s: Subscription): Unit = {\n      subscription = s\n      s.request(10)  // Request elements with backpressure\n    }\n\n    override def onNext(item: Int): Unit = {\n      println(s\"Received: $item\")\n      subscription.request(1)  // Request next element\n    }\n\n    override def onError(t: Throwable): Unit =\n      println(s\"Error: ${t.getMessage}\")\n\n    override def onComplete(): Unit =\n      println(\"Completed\")\n  })\n}\n```\n\n**With Custom Buffer Configuration:**\n\n```scala 3\nimport in.rcard.yaes.{Flow, Channel}\nimport in.rcard.yaes.FlowPublisher.asPublisher\n\nval flow = Flow(1 to 100: _*)\n\nval publisher = flow.asPublisher(\n  bufferCapacity = Channel.Type.Bounded(32, Channel.OverflowStrategy.SUSPEND)\n)\n```\n\n**Using Factory Methods:**\n\n```scala 3\nimport in.rcard.yaes.{Flow, FlowPublisher}\n\n// Default buffer capacity (16, SUSPEND)\nval publisher1 = FlowPublisher.fromFlow(flow)\n\n// Custom buffer capacity\nval publisher2 = FlowPublisher.fromFlow(\n  flow,\n  Channel.Type.Bounded(64, Channel.OverflowStrategy.SUSPEND)\n)\n```\n\nKey features:\n- **Cold execution**: Each subscription triggers independent Flow execution\n- **Demand-driven**: Respects subscriber's `request(n)` for backpressure\n- **Buffered**: Channel buffers elements between Flow producer and Subscriber consumer\n- **Cancellable**: Subscribers can cancel to stop emission and clean up resources\n- **Error propagation**: Flow errors are propagated to subscriber's `onError`\n- **Concurrent**: Uses fibers internally for producer and consumer coordination\n\nFor comprehensive documentation including demand management, backpressure patterns, error handling, and best practices, see the [Reactive Streams Integration documentation](https://rcardin.github.io/yaes/data-structures.html#reactive-streams-integration).\n\n#### Error Handling\n\nChannel operations can raise `ChannelClosed` errors. These must be handled using the `Raise` effect:\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Channel.ChannelClosed\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\nval result: ChannelClosed | String = Raise.run {\n  Async.run {\n    val channel = Channel.unbounded[String]()\n    channel.close()\n    \n    // This will raise ChannelClosed\n    channel.send(\"too late!\")\n  }\n}\n```\n\n#### Practical Example: Pipeline Pattern\n\nChannels are excellent for building data processing pipelines:\n\n```scala 3\nimport in.rcard.yaes.Channel\nimport in.rcard.yaes.Channel.Producer\nimport in.rcard.yaes.Async.*\nimport in.rcard.yaes.Raise.*\n\ncase class User(id: Int, name: String)\n\ndef processUsers(): Unit = Raise.run {\n  Async.run {\n    // Stage 1: Generate user IDs\n    val idsChannel = Channel.produce[Int] {\n      (1 to 100).foreach(Producer.send)\n    }\n\n    // Stage 2: Fetch users (with bounded channel for backpressure)\n    val usersChannel = Channel.produceWith[User](Channel.Type.Bounded(10)) {\n      idsChannel.foreach { id =\u003e\n        val user = User(id, s\"User$id\") // Simulate fetch\n        Producer.send(user)\n      }\n    }\n\n    // Stage 3: Process users\n    usersChannel.foreach { user =\u003e\n      println(s\"Processing: ${user.name}\")\n      // Do actual processing...\n    }\n  }\n}\n```\n\n## Contributing\n\nIf you want to contribute to the project, please do it 🙏! Any help is welcome.\n\n## Acknowledgments\n\nMany smart engineers helped me with thei ideas and suggestions. I want to thank them all. In particular, I want to thank:\n\n- [Daniel Ciocîrlan](https://rockthejvm.com/): He's the first that saw something in me and gave me the opportunity to work with him. He's a great mentor and a great friend.\n- [Simon Vergauwen](https://github.com/nomisRev): He's a great engineer. Now, he's focused on Kotlin and the Arrow Kt library, which drove many of the ideas behind the λÆS library.\n- [Jon Pretty](https://github.com/propensive): We shared some great ideas about the [Raise] effect. I love the way he thinks about programming.\n- [Noel Welsh](https://noelwelsh.com/): We chat about the `Raise` effect and the way to handle errors in a functional way. He's a great engineer and a great person.\n- [Flavio Brasil](https://github.com/fwbrasil): He creates the Kyo library, which is a great inspiration for the λÆS library. He helped me a lot with good suggestions and ideas.\n\nThanks guys! 🙏\n\n## References\n\nIt follows some quotations and links to valuable resources to understand the concepts behind the library:\n\n1. [Introduction to Abilities: A Mental Model - What do we mean by effects](https://www.unison-lang.org/docs/fundamentals/abilities/#what-do-we-mean-by-effects):\n   \u003e […] You might think of an effectful computation as one which performs an action outside of its local scope compared to one which simply returns a calculable value. […] So when functional programmers talk about managing effects, they're talking about expressing the basic logic of their programs within some guard rails provided by data structures or programming language constructs.\n\n2. [Abilities, not monads](https://softwaremill.com/trying-out-unison-part-3-effects-through-abilities/)\n   \u003e […] Unison offers abilities, which are an implementation of algebraic effects. An ability is a property of a function (it's not part of the value's type!).\n   \n3. [Abilities for the monadically inclined](https://www.unison-lang.org/docs/fundamentals/abilities/for-monadically-inclined/)\n\n4. [Effect Oriented Programming, by Bill Frasure, Bruce Eckel, James Ward](https://effectorientedprogramming.com/)\n   \u003e An Effect is an unpredictable interaction, usually with an external system. […] An Effect System manages Effects by wrapping these calls. […] Unpredictable elements are Side Effects. […] A Side Effect occurs when calling a function changes the context of that function. […] There’s an important difference: Side Effects are unmanaged and Effects are managed. A Side Effect “just happens” but an Effect is explicitly tracked and controlled. […] With an Effect System, we manage Effect behavior by putting that Effect in a kind of box. […] An Effect System provides a set of components that replace Side-Effecting functions in standard libraries, along with the structure for managing Effectful functions that you write. An Effect System enables us to add almost any functionality to a program. […] Managing an Effect means we not only control what results are produced by a function like `nextInt()`, but also when those results are produced. The control of when is called deferred execution. Deferred execution is part of the solution for easily attaching functionality to an existing program. […] Deferring the execution of an Effect is part of what enables us to add functionality to that Effect. […] If Effects ran immediately, we could not freely add behaviors. […] When we manage an Effect, we hold a value that represents something that can be run but hasn’t yet.\n   \n5. [An Introduction to Algebraic Effects and Handlers](https://www.eff-lang.org/handlers-tutorial.pdf)\n   \u003e The idea behind it is that operation calls do not perform actual effects (e.g. printing to an output device), but behave as signals that propagate outwards until they reach a handler with a matching clause\n\n6. [CanThrow Capabilities](https://docs.scala-lang.org/scala3/reference/experimental/canthrow.html)\n\n7. [Essential Effects, by Adam Rosien](https://essentialeffects.dev/)\n   \u003e we’ll distinguish two aspects of code: computing values and interacting with the environment. At the same time, we’ll talk about how transparent, or not, our code can be in describing these aspects. […] To understand what plusOne does, you don’t have to look anywhere except the (literal) definition of plusOne. There are no references to anything outside of it. This is sometimes referred to as local reasoning. Under substitution, programs mean the same thing if they evaluate to the same value. 13 + 1 means exactly the same thing as 14. So does plusOne(12 + 1), or even (12 + 1) + 1. This is known as referential transparency. […] If we impose some conditions, we can tame the side effects into something safer; we’ll call these effects. […] The type of the program should tell us what kind of effects the program will perform, in addition to the type of the value it will produce. If the behavior we want relies upon some externally-visible side effect, we separate describing the effects we want to happen from actually making them happen. We can freely substitute the description of effects until the point we run them. […] We delay the side effect so it executes outside of any evaluation, ensuring substitution still holds within. We’ll call these conditions the Effect Pattern. […] We can construct individual effects, and run them, but how do we combine them? We may want to modify the output of an effect (via map), or use the output of an effect to create a new effect (via flatMap). But be careful! Composing effects must not execute them.\n\n8. [Koka Language - 3.4. Effect Handlers](https://koka-lang.github.io/koka/doc/book.html#sec-handlers)\n   \u003e Effect handlers are a novel way to define control-flow abstractions and dynamic binding as user defined handlers – no need anymore to add special compiler extensions for exceptions, iterators, async-await, probabilistic programming, etc. Moreover, these handlers can be composed freely so the interaction between, say, async-await and exceptions are well-defined.\n\n9. [Algebraic Effects from Scratch by Kit Langton](https://www.youtube.com/watch?v=qPvPdRbTF-E\u0026t=763s)\n\n10. [Effekt: Capability-passing style for type- and effect-safe, extensible effect handlers in Scala](https://www.cambridge.org/core/journals/journal-of-functional-programming/article/effekt-capabilitypassing-style-for-type-and-effectsafe-extensible-effect-handlers-in-scala/A19680B18FB74AD95F8D83BC4B097D4F)\n\n11. [Object-capability model](https://en.wikipedia.org/wiki/Object-capability_model)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frcardin%2Fyaes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frcardin%2Fyaes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frcardin%2Fyaes/lists"}