Skip to content

Commit

Permalink
Improvements to typed errors docs (#173)
Browse files Browse the repository at this point in the history
This PR introduces a few improvements to the _typed errors_ section,
based on discussions I had with attendees to KotlinConf'23.
- Separates the _Create your own errors_ into its own page, and includes
the discussion in #161.
- Makes it more clear what we mean with each term (logical failure,
success...).
- More slowly builds the differences between Either/Result/Raise...

---------

Co-authored-by: Simon Vergauwen <[email protected]>
  • Loading branch information
serras and nomisRev authored May 6, 2023
1 parent 6400eb5 commit 8df1aad
Show file tree
Hide file tree
Showing 22 changed files with 699 additions and 395 deletions.
4 changes: 2 additions & 2 deletions content/docs/learn/typed-errors/either_ior.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ or `Ior`.

```mermaid
graph LR;
raise{{"Raise<E>.() -> A"}};
other{{"Either<E, A> / Ior<E, A>"}};
raise{{"Raise&lt;E&gt;.() -> A"}};
other{{"Either&lt;E, A&gt; / Ior&lt;E, A&gt;"}};
raise-->|either / ior|other;
other-->|".bind()"|raise;
```
Expand Down
147 changes: 147 additions & 0 deletions content/docs/learn/typed-errors/own-error-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
description: Writing your own DSLs with Raise.
sidebar_position: 5
---

# Creating your own error wrappers

<!--- TEST_NAME OwnErrorsTest -->

`Raise` is a powerful tool that allows us to create our own DSLs to raise typed errors.
It easily allows integration with existing libraries and frameworks that offer similar data types like `Either` or even your own custom types.
For example, let's take a popular ADT often used in the front end, a type that models `Loading`, `Content`, or `Failure`, often abbreviated as `LCE`.

<!--- INCLUDE
import arrow.core.raise.Raise
import arrow.core.raise.ensure
import arrow.core.raise.recover
import io.kotest.matchers.shouldBe
import kotlin.experimental.ExperimentalTypeInference
-->
```kotlin
sealed interface Lce<out E, out A> {
object Loading : Lce<Nothing, Nothing>
data class Content<A>(val value: A) : Lce<Nothing, A>
data class Failure<E>(val error: E) : Lce<E, Nothing>
}
```

## Basic functionality

Let's say that once a `Failure` or `Loading` case is encountered, we want to short-circuit and not continue with the computation.
It's easy to define a `Raise` instance for `Lce` that does just that. We'll use the composition pattern to do this **without** context receivers.
Since we need to _raise_ both `Lce.Loading` and `Lce.Failure`, our `Raise` instance will need to be able to `raise` `Lce<E, Nothing>`, and we wrap that in a `LceRaise` class.
Within that class, a `bind` function can be defined to short-circuit any encountered `Failure` or `Loading` case or otherwise return the `Content` value.

```kotlin
@JvmInline
value class LceRaise<E>(val raise: Raise<Lce<E, Nothing>>) : Raise<Lce<E, Nothing>> by raise {
fun <A> Lce<E, A>.bind(): A = when (this) {
is Lce.Content -> value
is Lce.Failure -> raise.raise(this)
Lce.Loading -> raise.raise(Lce.Loading)
}
}
```

All that is required now is a DSL function. We can use the `recover` or `fold` function to summon an instance of `RaiseLce<E, Nothing>` from the `Raise` type class.
We wrap the `block` in an `Lce.Content` value and return any encountered `Lce<E, Nothing>` value. We can call `block` by wrapping `Raise<Lce<E, Nothing>>` in `LceRaise`.

```kotlin
@OptIn(ExperimentalTypeInference::class)
inline fun <E, A> lce(@BuilderInference block: LceRaise<E>.() -> A): Lce<E, A> =
recover({ Lce.Content(block(LceRaise(this))) }) { e: Lce<E, Nothing> -> e }
```

We can now use this DSL to compose our computations and `Lce` values in the same way as we've discussed above in this document.
Furthermore, since this DSL is built on top of `Raise`, we can use all the functions we've discussed above.

```kotlin
fun example() {
lce {
val a = Lce.Content(1).bind()
val b = Lce.Content(1).bind()
a + b
} shouldBe Lce.Content(2)

lce {
val a = Lce.Content(1).bind()
ensure(a > 1) { Lce.Failure("a is not greater than 1") }
a + 1
} shouldBe Lce.Failure("a is not greater than 1")
}
```
<!--- KNIT example-own-errors-01.kt -->
<!--- TEST assert -->

If we'd used _context receivers_, defining this DSL would be even more straightforward, and we could use the `Raise` type class directly.

```kotlin
context(Raise<Lce<E, Nothing>>)
fun <E, A> Lce<E, A>.bind(): A = when (this) {
is Lce.Content -> value
is Lce.Failure -> raise(this)
Lce.Loading -> raise(Lce.Loading)
}

inline fun <E, A> lce(@BuilderInference block: Raise<Lce<E, Nothing>>.() -> A): Lce<E, A> =
recover({ Lce.Content(block(this)) }) { e: Lce<E, Nothing> -> e }
```

## Reflections on `Failure`

The reason to choose `Lce<E, Nothing>` as type for `Failure` allows for a DSL that has multiple errors.
Let's consider now a type similar to `Lce`, but with additional states which are not considered success.

```kotlin
DialogResult<out T>
Positive<out T>(value: T) : DialogResult<T>
Neutral : DialogResult<Nothing>
Negative : DialogResult<Nothing>
Cancelled: DialogResult<Nothing>
```

We can now not really conveniently provide `Raise` over the _flat_ type `DialogResult`, and are kind-of forced to use `DialogResult<Nothing>`. However, if we stratify our type differently,

```kotlin
DialogResult<out T>
Positive<out T>(value: T) : DialogResult<T>
Error : DialogResult<Nothing>
Neutral : Error
Negative : Error
Cancelled: Error
```

We can again benefit from `Raise<DialogResult.Error>`, and the reason that this is **much** more desirable, it that you can now also interop with `Either`!

```kotlin
dialogResult {
val x: DialogResult.Positive(1).bind()
val y: Int = DialogResult.Error.left().bind()
x + y
}
```

That can be useful if you need to for example want to _accumulate errors_, you can now benefit from the default behavior in Kotlin.

```kotlin
fun dialog(int: Int): DialogResult<Int> =
if(int % 2 == 0) DialogResult.Positive(it) else Dialog.Neutral

val res: Either<NonEmptyList<DialogResult.Error>, NonEmptyList<Int>> =
listOf(1, 2, 3).mapOrAccumulate { i: Int ->
dialog(it).getOrElse { raise(it) }
}

dialogResult {
res.mapLeft { ... }.bind()
}
```

:::info Further discussion

This section was created as a response to
[this issue in our repository](https://github.com/arrow-kt/arrow-website/issues/161).
Let's create great docs for Arrow together!

:::
Loading

0 comments on commit 8df1aad

Please sign in to comment.