-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improvements to typed errors docs #173
Merged
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! | ||
|
||
::: |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha, great!
I wanted to add some more details to this doc, we can do it here or in a subsequent PR.
Lce
to work overLceRaise<Any?>
and expose it overRaise<E>
to make it interopt with all other DSLs. See Quiver, https://github.com/cashapp/quiver/pull/26/files#diff-2a7ca3e3751a2611788369c9a4c63f27ee97ed2403d24e58d1476e01f57937b0R48DialogResult
can just have abind()
function defined overRaise<E>
when we have context receivers