Skip to content
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

Remove or warn mentions of context receivers #346

Merged
merged 1 commit into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions content/docs/learn/design/effects-contexts.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sidebar_position: 2
<link rel="canonical" href="https://www.47deg.com/blog/effects-contexts/" />
</head>

:::note This article was originally published in the [47 Degrees blog](https://www.47deg.com/blog/effects-contexts/).
:::note This article was originally published in the [47 Degrees blog](https://www.47deg.com/blog/effects-contexts/), and subsequently updated to replace context receivers with context parameters.

:::

Expand Down Expand Up @@ -188,14 +188,14 @@ Then we cannot define a context requiring `Environment<ConnectionParams>` and `E

### Looking at the future

The future looks quite bright for Kotliners in this respect. For a few versions now, the language includes [_context receivers_](https://github.com/Kotlin/KEEP/issues/259), which would allow a cleaner design for what we are describing using subtyping. Using context receiver, we're able to state:
The future looks quite bright for Kotliners in this respect. For a few versions now, the language includes [_context parameters](https://github.com/Kotlin/KEEP/issues/367), which would allow a cleaner design for what we are describing using subtyping. Using context receiver, we're able to state:

```kotlin
context(Database, Logger)
context(db: Database, logger: Logger)
fun User.saveInDb() { ... }
```

and inject the values by simply nesting the calls to `db` and `stdoutLogger`. Note that sometimes you need a more robust `with` function than the one provided by the standard library, like [this one](https://gist.github.com/carbaj03/4ebd0f8da17c351d4235e1bedd9a36b5), which admits subtyping within contexts.
and inject the values by simply nesting the calls to `db` and `stdoutLogger`, or simply using the `context` function.

## Contexts, effects, algebras

Expand Down
2 changes: 1 addition & 1 deletion content/docs/learn/design/receivers-flatmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sidebar_position: 3
<link rel="canonical" href="https://xebia.com/blog/the-suspend-receivers-style-in-kotlin/" />
</head>

:::note This article was originally published at [Xebia's blog](https://xebia.com/blog/the-suspend-receivers-style-in-kotlin/).
:::note This article was originally published at [Xebia's blog](https://xebia.com/blog/the-suspend-receivers-style-in-kotlin/). This article mentions context receivers, which are deprecated, and shall be updated once context parameters are released.

:::

Expand Down
6 changes: 3 additions & 3 deletions content/docs/learn/typed-errors/own-error-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ fun example() {
<!--- 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.
If we'd used _context parameters, defining this DSL would be even more straightforward, and we could use the `Raise` type class directly.

```kotlin
context(Raise<Lce<E, Nothing>>)
```
context(_: Raise<Lce<E, Nothing>>)
fun <E, A> Lce<E, A>.bind(): A = when (this) {
is Lce.Content -> value
is Lce.Failure -> raise(this)
Expand Down
55 changes: 24 additions & 31 deletions content/docs/learn/typed-errors/working-with-typed-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ as their first type parameter.
The second approach is describing errors as part of the _computation context_ of the function.
In that case the ability to finish with logical failures is represented by having `Raise<E>`
be part of the context or scope of the function. Kotlin offers two choices here: we can use
an extension receiver or using the more modern context receivers.
an extension receiver, and in the future we may use context parameters.

```
// Raise<UserNotFound> is extension receiver
fun Raise<UserNotFound>.findUser(id: UserId): User
// Raise<UserNotFound> is context receiver
context(Raise<UserNotFound>) fun findUser(id: UserId): User
// Raise<UserNotFound> is context parameter
context(_: Raise<UserNotFound>) fun findUser(id: UserId): User
```

Let's define a simple program that _raises_ a _logical failure_ of `UserNotFound` or returns a `User`. We can represent this both as a value `Either<UserNotFound, User>`, and as a _computation_ (using `Raise<UserNotFound>`).
Expand Down Expand Up @@ -202,21 +202,14 @@ fun example() {
<!--- TEST assert -->

Without context receivers, these functions look pretty different depending on if we use `Raise` or `Either`. This is because we sacrifice our _extension receiver_ for `Raise`.
And thus, the `Raise` based computation cannot be an extension function on `User`. With context receivers, we could've defined it as:
And thus, the `Raise` based computation cannot be an extension function on `User`.
In the future, context parameters should allow us to define the function as follows:

<!--- INCLUDE
import arrow.core.raise.Raise
import arrow.core.raise.ensure

data class User(val id: Long)
data class UserNotFound(val message: String)
-->
```kotlin
context(Raise<UserNotFound>)
```
context(_: Raise<UserNotFound>)
fun User.isValid(): Unit =
ensure(id > 0) { UserNotFound("User without a valid id: $id") }
```
<!--- KNIT example-typed-errors-04.kt -->

`ensureNotNull` takes a _nullable value_ and a _lazy_ `UserNotFound` value. When the value is null, the _computation_ will result in a _logical failure_ of `UserNotFound`.
Otherwise, the value will be _smart-casted_ to non-null, and you can operate on it without checking nullability.
Expand Down Expand Up @@ -256,7 +249,7 @@ fun example() {
)
}
```
<!--- KNIT example-typed-errors-05.kt -->
<!--- KNIT example-typed-errors-04.kt -->
<!--- TEST assert -->

## Running and inspecting results
Expand Down Expand Up @@ -294,7 +287,7 @@ fun example() {
)
}
```
<!--- KNIT example-typed-errors-06.kt -->
<!--- KNIT example-typed-errors-05.kt -->
<!--- TEST assert -->

:::info Fold over all possible cases
Expand Down Expand Up @@ -328,7 +321,7 @@ fun example() {
either { error() } shouldBe UserNotFound.left()
}
```
<!--- KNIT example-typed-errors-07.kt -->
<!--- KNIT example-typed-errors-06.kt -->
<!--- TEST assert -->

<!--- INCLUDE
Expand Down Expand Up @@ -356,7 +349,7 @@ computation with `Raise`, is achieved via the `.bind()` extension function.
```kotlin
fun Raise<UserNotFound>.res(): User = user.bind()
```
<!--- KNIT example-typed-errors-08.kt -->
<!--- KNIT example-typed-errors-07.kt -->

In fact, to define a result with a wrapper type, we recommend to use one
of the runners (`either`, `ior`, et cetera), and use `.bind()` to "inject"
Expand Down Expand Up @@ -385,7 +378,7 @@ val maybeSeven: Either<Problem, Int> = either {
maybeTwo.bind() + maybeFive.bind()
}
```
<!--- KNIT example-typed-errors-09.kt -->
<!--- KNIT example-typed-errors-08.kt -->

```mermaid
graph LR;
Expand Down Expand Up @@ -429,7 +422,7 @@ fun problematic(n: Int): Either<Problem, Int?> =
}
}
```
<!--- KNIT example-typed-errors-10.kt -->
<!--- KNIT example-typed-errors-09.kt -->

:::

Expand Down Expand Up @@ -494,7 +487,7 @@ suspend fun example() {
}) { e: UserNotFound -> null } shouldBe User(1)
}
```
<!--- KNIT example-typed-errors-11.kt -->
<!--- KNIT example-typed-errors-10.kt -->
<!--- TEST assert -->

Default to `null` is typically not desired since we've effectively swallowed our _logical failure_ and ignored our error. If that was desirable, we could've used nullable types initially.
Expand Down Expand Up @@ -538,7 +531,7 @@ fun example() {
.recover { _: UserNotFound -> raise(OtherError) } shouldBe OtherError.left()
}
```
<!--- KNIT example-typed-errors-12.kt -->
<!--- KNIT example-typed-errors-11.kt -->
<!--- TEST assert -->

The type system now tracks that a new error of `OtherError` might have occurred, but we recovered from any possible errors of `UserNotFound`. This is useful across application layers or in the service layer, where we might want to `recover` from a `DatabaseError` with a `NetworkError` when we want to load data from the network when a database operation failed.
Expand All @@ -565,7 +558,7 @@ suspend fun Raise<OtherError>.recovery(): User =
fetchUser(-1)
}) { _: UserNotFound -> raise(OtherError) }
```
<!--- KNIT example-typed-errors-13.kt -->
<!--- KNIT example-typed-errors-12.kt -->

:::tip DSLs everywhere
Since recovery for both `Either` and `Raise` is DSL based, you can also call `bind` or `raise` from both.
Expand Down Expand Up @@ -624,7 +617,7 @@ suspend fun insertUser(username: String, email: String): Either<UserAlreadyExist
else throw e
}
```
<!--- KNIT example-typed-errors-14.kt -->
<!--- KNIT example-typed-errors-13.kt -->

This pattern allows us to turn exceptions we want to track into _typed errors_, and things that are **truly** exceptional remain exceptional.

Expand Down Expand Up @@ -672,7 +665,7 @@ fun example() {
(1..10).mapOrAccumulate { isEven2(it).bind() } shouldBe errors
}
```
<!--- KNIT example-typed-errors-15.kt -->
<!--- KNIT example-typed-errors-14.kt -->
<!--- TEST assert -->

We can also provide custom logic to accumulate the errors, typically when we have custom types.
Expand Down Expand Up @@ -715,7 +708,7 @@ fun example() {
(1..10).mapOrAccumulate(MyError::plus) { isEven2(it).bind() } shouldBe error
}
```
<!--- KNIT example-typed-errors-16.kt -->
<!--- KNIT example-typed-errors-15.kt -->
<!--- TEST assert -->

:::tip Accumulating errors but not values
Expand All @@ -734,7 +727,7 @@ fun example() = either {
}
}
```
<!--- KNIT example-typed-errors-17.kt -->
<!--- KNIT example-typed-errors-16.kt -->
<!--- TEST assert -->

:::
Expand All @@ -750,7 +743,7 @@ As a guiding example, let's consider information about a user, where the name sh
```kotlin
data class User(val name: String, val age: Int)
```
<!--- KNIT example-typed-errors-18.kt -->
<!--- KNIT example-typed-errors-17.kt -->

It's customary to define the different problems that may arise from validation as a sealed interface:

Expand Down Expand Up @@ -790,7 +783,7 @@ fun example() {
User("", -1) shouldBe Left(UserProblem.EmptyName)
}
```
<!--- KNIT example-typed-errors-19.kt -->
<!--- KNIT example-typed-errors-18.kt -->
<!--- TEST assert -->

<!--- INCLUDE
Expand Down Expand Up @@ -833,7 +826,7 @@ fun example() {
User("", -1) shouldBe Left(nonEmptyListOf(UserProblem.EmptyName, UserProblem.NegativeAge(-1)))
}
```
<!--- KNIT example-typed-errors-20.kt -->
<!--- KNIT example-typed-errors-19.kt -->
<!--- TEST assert -->

:::tip Error accumulation and concurrency
Expand Down Expand Up @@ -870,7 +863,7 @@ fun example() {
intError shouldBe Either.Left("problem".length)
}
-->
<!--- KNIT example-typed-errors-21.kt -->
<!--- KNIT example-typed-errors-20.kt -->
<!--- TEST assert -->

A very common pattern is using `withError` to "bridge" validation errors of sub-components into validation errors of the larger value.
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ kotest = "5.9.1"
kotlin = "2.1.0"
knit = "0.5.0"
arrow = "1.2.4"
ksp = "2.0.21-1.0.28"
ksp = "2.1.0-RC2-1.0.28"
suspendapp = "0.4.0"
kotlinKafka = "0.4.0"
cache4k = "0.13.0"
Expand Down
4 changes: 0 additions & 4 deletions guide/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,4 @@ tasks {
setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError"))
}
}

withType<KotlinCompile>().configureEach {
compilerOptions.freeCompilerArgs.add("-Xcontext-receivers")
}
}
30 changes: 26 additions & 4 deletions guide/src/test/kotlin/examples/example-typed-errors-04.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,34 @@
// This file was automatically generated from working-with-typed-errors.md by Knit tool. Do not edit.
package arrow.website.examples.exampleTypedErrors04

import arrow.core.Either
import arrow.core.left
import arrow.core.raise.ensureNotNull
import arrow.core.raise.either
import arrow.core.raise.Raise
import arrow.core.raise.ensure
import arrow.core.raise.fold
import io.kotest.assertions.fail
import io.kotest.matchers.shouldBe

data class User(val id: Long)
data class UserNotFound(val message: String)

context(Raise<UserNotFound>)
fun User.isValid(): Unit =
ensure(id > 0) { UserNotFound("User without a valid id: $id") }
fun process(user: User?): Either<UserNotFound, Long> = either {
ensureNotNull(user) { UserNotFound("Cannot process null user") }
user.id // smart-casted to non-null
}

fun Raise<UserNotFound>.process(user: User?): Long {
ensureNotNull(user) { UserNotFound("Cannot process null user") }
return user.id // smart-casted to non-null
}

fun example() {
process(null) shouldBe UserNotFound("Cannot process null user").left()

fold(
{ process(User(1)) },
{ _: UserNotFound -> fail("No logical failure occurred!") },
{ i: Long -> i shouldBe 1L }
)
}
27 changes: 12 additions & 15 deletions guide/src/test/kotlin/examples/example-typed-errors-05.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,30 @@
package arrow.website.examples.exampleTypedErrors05

import arrow.core.Either
import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.left
import arrow.core.raise.ensureNotNull
import arrow.core.raise.either
import arrow.core.raise.Raise
import arrow.core.raise.fold
import io.kotest.assertions.fail
import io.kotest.matchers.shouldBe

object UserNotFound
data class User(val id: Long)
data class UserNotFound(val message: String)

fun process(user: User?): Either<UserNotFound, Long> = either {
ensureNotNull(user) { UserNotFound("Cannot process null user") }
user.id // smart-casted to non-null
}
val error: Either<UserNotFound, User> = UserNotFound.left()

fun Raise<UserNotFound>.process(user: User?): Long {
ensureNotNull(user) { UserNotFound("Cannot process null user") }
return user.id // smart-casted to non-null
}
fun Raise<UserNotFound>.error(): User = raise(UserNotFound)

fun example() {
process(null) shouldBe UserNotFound("Cannot process null user").left()
when (error) {
is Left -> error.value shouldBe UserNotFound
is Right -> fail("A logical failure occurred!")
}

fold(
{ process(User(1)) },
{ _: UserNotFound -> fail("No logical failure occurred!") },
{ i: Long -> i shouldBe 1L }
block = { error() },
recover = { e: UserNotFound -> e shouldBe UserNotFound },
transform = { _: User -> fail("A logical failure occurred!") }
)
}
12 changes: 2 additions & 10 deletions guide/src/test/kotlin/examples/example-typed-errors-06.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import arrow.core.Either.Right
import arrow.core.left
import arrow.core.raise.Raise
import arrow.core.raise.fold
import arrow.core.raise.either
import io.kotest.assertions.fail
import io.kotest.matchers.shouldBe

Expand All @@ -18,14 +19,5 @@ val error: Either<UserNotFound, User> = UserNotFound.left()
fun Raise<UserNotFound>.error(): User = raise(UserNotFound)

fun example() {
when (error) {
is Left -> error.value shouldBe UserNotFound
is Right -> fail("A logical failure occurred!")
}

fold(
block = { error() },
recover = { e: UserNotFound -> e shouldBe UserNotFound },
transform = { _: User -> fail("A logical failure occurred!") }
)
either { error() } shouldBe UserNotFound.left()
}
12 changes: 5 additions & 7 deletions guide/src/test/kotlin/examples/example-typed-errors-07.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,18 @@ package arrow.website.examples.exampleTypedErrors07
import arrow.core.Either
import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.left
import arrow.core.right
import arrow.core.raise.Raise
import arrow.core.raise.fold
import arrow.core.raise.either
import arrow.core.raise.fold
import io.kotest.assertions.fail
import io.kotest.matchers.shouldBe

object UserNotFound
data class User(val id: Long)

val error: Either<UserNotFound, User> = UserNotFound.left()
val user: Either<UserNotFound, User> = User(1).right()

fun Raise<UserNotFound>.error(): User = raise(UserNotFound)
fun Raise<UserNotFound>.user(): User = User(1)

fun example() {
either { error() } shouldBe UserNotFound.left()
}
fun Raise<UserNotFound>.res(): User = user.bind()
Loading