Skip to content

Commit

Permalink
orDie combinator (#902)
Browse files Browse the repository at this point in the history
- Removes `Abort[E]` effect for any `E`
- If `E` is `Throwable`, throw any failures
- If `E` is not `Throwable`, throw a `PanicException` that contains `E`
  • Loading branch information
johnhungerford authored Dec 7, 2024
1 parent 997bbcb commit e21e85b
Show file tree
Hide file tree
Showing 4 changed files with 591 additions and 321 deletions.
100 changes: 70 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,6 @@ Int < Abort[Absent]
String < (Abort[Absent] & IO)
```

> Note: The naming convention for effect types is the plural form of the functionalities they manage.
Any type `T` is automatically considered to be of type `T < Any`, where `Any` denotes an absence of pending effects. In simpler terms, this means that every value in Kyo is automatically a computation, but one without any effects that you need to handle.

This design choice streamlines your code by removing the necessity to differentiate between pure values and computations that may have effects. So, when you're dealing with a value of type `T < Any`, you can safely `eval` the pure value directly, without worrying about handling any effects.
Expand Down Expand Up @@ -419,8 +417,6 @@ The `defer` method in Kyo mirrors Scala's `for`-comprehensions in providing a co

The `kyo-direct` module is constructed as a wrapper around [dotty-cps-async](https://github.com/rssh/dotty-cps-async).

> Note: `defer` is currently the only user-facing macro in Kyo. All other features use regular language constructs.
### Defining an App

`KyoApp` offers a structured approach similar to Scala's `App` for defining application entry points. However, it comes with added capabilities, handling a suite of default effects. As a result, the `run` method within `KyoApp` can accommodate various effects, such as IO, Async, Resource, Clock, Console, Random, Timer, and Aspect.
Expand Down Expand Up @@ -3334,6 +3330,8 @@ trait HelloService:
def sayHelloTo(saluee: String): Unit < (IO & Abort[Throwable])

object HelloService:
val live = Layer(Live)

object Live extends HelloService:
override def sayHelloTo(saluee: String): Unit < (IO & Abort[Throwable]) =
Kyo.suspendAttempt { // Adds IO & Abort[Throwable] effect
Expand All @@ -3342,59 +3340,101 @@ object HelloService:
end Live
end HelloService

val keepTicking: Nothing < (Console & Async & Abort[IOException]) =
val keepTicking: Nothing < (Async & Abort[IOException]) =
(Console.print(".") *> Kyo.sleep(1.second)).forever

val effect: Unit < (Console & Async & Resource & Abort[Throwable] & Env[NameService]) =
val effect: Unit < (Async & Resource & Abort[Throwable] & Env[HelloService]) =
for
nameService <- Kyo.service[NameService] // Adds Env[NameService] effect
_ <- keepTicking.forkScoped // Adds Console, Async, and Resource effects
saluee <- Console.readLine // Uses Console effect
nameService <- Kyo.service[HelloService] // Adds Env[NameService] effect
_ <- keepTicking.forkScoped // Adds Async, Abort[IOException], and Resource effects
saluee <- Console.readln
_ <- Kyo.sleep(2.seconds) // Uses Async (semantic blocking)
_ <- nameService.sayHelloTo(saluee) // Adds Abort[Throwable] effect
_ <- nameService.sayHelloTo(saluee) // Lifts Abort[IOException] to Abort[Throwable]
yield ()
end for
end effect

// There are no combinators for handling IO or blocking Async, since this should
// be done at the edge of the program
IO.Unsafe.run { // Handles IO
Async.runAndBlock(Duration.Inf) { // Handles Async
IO.Unsafe.run { // Handles IO
Async.runAndBlock(Duration.Inf) { // Handles Async
Kyo.scoped { // Handles Resource
effect
.provideAs[HelloService](HelloService.Live) // Handles Env[HelloService]
.catchAbort((thr: Throwable) => // Handles Abort[Throwable]
Kyo.debug(s"Failed printing to console: ${throwable}")
)
.provideDefaultConsole // Handles Console
Memo.run: // Handles Memo (introduced by .provide, below)
effect
.catching((thr: Throwable) => // Handles Abort[Throwable]
Kyo.debug(s"Failed printing to console: ${throwable}")
)
.provide(HelloService.live) // Works like ZIO[R,E,A]#provide, but adds Memo effect
}
}
}
```

### Failure conversions
### Error handling

One notable departure from the ZIO API worth calling out is a set of combinators for converting between failure effects. Whereas ZIO has a single channel for describing errors, Kyo has different effect types that can describe failure in the basic sense of "short-circuiting": `Abort` and `Choice` (an empty `Seq` being equivalent to a short-circuit). `Abort[Absent]` can also be used like `Choice` to model short-circuiting an empty result. It's useful to be able to move between these effects easily, so `kyo-combinators` provides a number of extension methods, usually in the form of `def effect1ToEffect2`.
Whereas ZIO has a single channel for describing errors, Kyo has different effect types that can describe failure in the basic sense of "short-circuiting": `Abort` and `Choice` (an empty `Seq` being equivalent to a short-circuit). `Abort[Absent]` can also be used like `Choice` to model short-circuiting an empty result.

For each of these, to handle the effect, lifting the result type to `Result`, `Seq`, and `Maybe`, use `.result`, `.handleChoice`, and `.maybe` respectively. Alternatively, you can convert between these different error types using methods usually in the form of `def effect1ToEffect2`, where `effect1` and `effect2` can be "abort" (`Abort[?]`), "absent" (`Abort[Absent]`), "empty" (`Choice`, when reduced to an empty sequence), and "throwable" (`Abort[Throwable]`).

Some examples:

```scala
val abortEffect: Int < Abort[String] = ???
val abortEffect: Int < Abort[String] = 1

// Converts failures to empty failure
val maybeEffect: Int < Abort[Absent] = abortEffect.abortToEmpty
val maybeEffect: Int < Abort[Absent] = abortEffect.abortToAbsent

// Converts an aborted Absent to an empty "choice"
val choiceEffect: Int < Choice = maybeEffect.absentToEmpty

// Converts empty failure to a single "choice" (or Seq)
val choiceEffect: Int < Choice = maybeEffect.emptyAbortToChoice
// Fails with exception if empty
val newAbortEffect: Int < (Choice & Abort[Throwable]) = choiceEffect.emptyToThrowable
```

To swallow errors à la ZIO's `orDie` and `resurrect` methods, you can use `orPanic` and `unpanic` respectively:

```scala
import kyo.*
import java.io.IOException

// Fails with Nil#head exception if empty and succeeds with Seq.head if non-empty
val newAbortEffect: Int < Abort[Throwable] = choiceEffect.choiceToThrowable
val abortEffect: Int < Abort[String | Throwable] = 1

// Throws a throwable Abort failure (will actually throw unless suspended)
val unsafeEffect: Int < Any = newAbortEffect.implicitAborts
// unsafeEffect will panic with a `PanicException(err)`
val unsafeEffect: Int < Any = abortEffect.orPanic

// Catch any suspended throws
val safeEffect: Int < Abort[Throwable] = unsafeEffect.explicitAborts
val safeEffect: Int < Abort[Throwable] = unsafeEffect.unpanic

// Use orPanic after forAbort[E] to swallow only errors of type E
val unsafeForThrowables: Int < Abort[String] = abortEffect.forAbort[Throwable].orPanic
```

Other error-handling methods are as follows:

```scala
import kyo.*

trait A
trait B
trait C

val effect: Int < Abort[A | B | C] = 1

val handled: Result[A | B | C, Int] < Any = effect.result
val mappedError: Int < Abort[String] = effect.mapAbort(_.toString)
val caught: Int < Any = effect.catching(_.toString.size)
val partiallyCaught: Int < Abort[A | B | C] = effect.catchingSome { case err if err.toString.size > 5 => 0 }

// Manipulate single types from within the union
val handledA: Result[A, Int] < Abort[B | C] = effect.forAbort[A].result
val caughtA: Int < Abort[B | C] = effect.forAbort[A].catching(_.toString.size)
val partiallyCaughtA: Int < Abort[A | B | C] = effect.forAbort[A].catchingSome { case err if err.toString.size > 5 => 0 }
val aToAbsent: Int < Abort[Absent | B | C] = effect.forAbort[A].toAbsent
val aToEmpty: Int < (Choice & Abort[B | C]) = effect.forAbort[A].toEmpty
val aToThrowable: Int < (Abort[Throwable | B | C]) = effect.forAbort[A].toThrowable
```


## Acknowledgements

Kyo's development was originally inspired by the paper ["Do Be Do Be Do"](https://arxiv.org/pdf/1611.09259.pdf) and its implementation in the [Unison](https://www.unison-lang.org/learn/language-reference/abilities-and-ability-handlers/) programming language. Kyo's design evolved from using interface-based effects to suspending concrete values associated with specific effects, making it more efficient when executed on the JVM.
Expand All @@ -3409,4 +3449,4 @@ License
-------

See the [LICENSE](https://github.com/getkyo/kyo/blob/master/LICENSE.txt) file for details.


Loading

0 comments on commit e21e85b

Please sign in to comment.