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

catch for EffectScope #2746

Merged
merged 19 commits into from
Jul 21, 2022
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
36 changes: 36 additions & 0 deletions arrow-libs/core/arrow-core/api/arrow-core.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import arrow.core.identity
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.coroutines.RestrictsSuspension
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmInline

/** Context of the [EagerEffect] DSL. */
@RestrictsSuspension
Expand Down Expand Up @@ -186,6 +188,56 @@ public interface EagerEffectScope<in R> {
*/
public suspend fun ensure(condition: Boolean, shift: () -> R): Unit =
if (condition) Unit else shift(shift())

/**
* Encloses an action for which you want to catch any `shift`.
* [attempt] is used in combination with [catch].
*
* ```
* attempt { ... } catch { ... }
* ```
*
* The [f] may `shift` into a different `EagerEffectScope`, giving
* the chance for a later [catch] to change the shifted value.
* This is useful to simulate re-throwing of exceptions.
*/
@OptIn(ExperimentalTypeInference::class)
public suspend fun <E, A> attempt(
i-walker marked this conversation as resolved.
Show resolved Hide resolved
@BuilderInference
f: suspend EagerEffectScope<E>.() -> A,
): suspend EagerEffectScope<E>.() -> A = f


/**
* When the [EagerEffect] has shifted with [R] it will [recover]
* the shifted value to [A], and when it ran the computation to
* completion it will return the value [A].
* [catch] is used in combination with [attempt].
*
* ```kotlin
* import arrow.core.Either
* import arrow.core.None
* import arrow.core.Option
* import arrow.core.Validated
* import arrow.core.continuations.eagerEffect
* import io.kotest.assertions.fail
* import io.kotest.matchers.shouldBe
*
* fun main() {
* eagerEffect<String, Int> {
* val x = Either.Right(1).bind()
* val y = Validated.Valid(2).bind()
* val z =
* attempt { None.bind { "Option was empty" } } catch { 0 }
* x + y + z
* }.fold({ fail("Shift can never be the result") }, { it shouldBe 3 })
* }
* ```
* <!--- KNIT example-eager-effect-scope-08.kt -->
*/
public infix fun <E, A> (suspend EagerEffectScope<E>.() -> A).catch(
recover: EagerEffectScope<R>.(E) -> A,
serras marked this conversation as resolved.
Show resolved Hide resolved
): A = eagerEffect(this).fold({ recover(it) }, ::identity)
}

/**
Expand All @@ -207,10 +259,11 @@ public interface EagerEffectScope<in R> {
* }.toEither() shouldBe (int?.right() ?: failure.left())
* }
* ```
* <!--- KNIT example-eager-effect-scope-08.kt -->
* <!--- KNIT example-eager-effect-scope-09.kt -->
*/
@OptIn(ExperimentalContracts::class)
public suspend fun <R, B : Any> EagerEffectScope<R>.ensureNotNull(value: B?, shift: () -> R): B {
contract { returns() implies (value != null) }
return value ?: shift(shift())
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import arrow.core.Validated
import arrow.core.identity
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmInline

/** Context of the [Effect] DSL. */
public interface EffectScope<in R> {
Expand Down Expand Up @@ -215,6 +217,55 @@ public interface EffectScope<in R> {
*/
public suspend fun ensure(condition: Boolean, shift: () -> R): Unit =
if (condition) Unit else shift(shift())

/**
* Encloses an action for which you want to catch any `shift`.
* [attempt] is used in combination with [catch].
*
* ```
* attempt { ... } catch { ... }
* ```
*
* The [f] may `shift` into a different `EffectScope`, giving
* the chance for a later [catch] to change the shifted value.
* This is useful to simulate re-throwing of exceptions.
*/
@OptIn(ExperimentalTypeInference::class)
public suspend fun <E, A> attempt(
@BuilderInference
f: suspend EffectScope<E>.() -> A,
): suspend EffectScope<E>.() -> A = f

/**
* When the [Effect] has shifted with [R] it will [recover]
* the shifted value to [A], and when it ran the computation to
* completion it will return the value [A].
* [catch] is used in combination with [attempt].
*
* ```kotlin
* import arrow.core.Either
* import arrow.core.None
* import arrow.core.Option
* import arrow.core.Validated
* import arrow.core.continuations.effect
* import io.kotest.assertions.fail
* import io.kotest.matchers.shouldBe
*
* suspend fun main() {
* effect<String, Int> {
* val x = Either.Right(1).bind()
* val y = Validated.Valid(2).bind()
* val z =
* attempt { None.bind { "Option was empty" } } catch { 0 }
* x + y + z
* }.fold({ fail("Shift can never be the result") }, { it shouldBe 3 })
* }
* ```
* <!--- KNIT example-effect-scope-09.kt -->
*/
public suspend infix fun <E, A> (suspend EffectScope<E>.() -> A).catch(
recover: suspend EffectScope<R>.(E) -> A,
): A = effect(this).fold({ recover(it) }, ::identity)
}

/**
Expand All @@ -236,10 +287,11 @@ public interface EffectScope<in R> {
* }.toEither() shouldBe (int?.right() ?: failure.left())
* }
* ```
* <!--- KNIT example-effect-scope-09.kt -->
* <!--- KNIT example-effect-scope-10.kt -->
*/
@OptIn(ExperimentalContracts::class)
public suspend fun <R, B : Any> EffectScope<R>.ensureNotNull(value: B?, shift: () -> R): B {
contract { returns() implies (value != null) }
return value ?: shift(shift())
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.boolean
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.long
import io.kotest.property.arbitrary.orNull
import io.kotest.property.arbitrary.string
import io.kotest.property.checkAll
Expand Down Expand Up @@ -57,6 +58,32 @@ class EagerEffectSpec : StringSpec({
}
}

"attempt - catch" {
checkAll(Arb.int(), Arb.long()) { i, l ->
eagerEffect<String, Int> {
attempt<Long, Int> {
shift(l)
} catch { ll ->
ll shouldBe l
i
}
}.runCont() shouldBe i
}
}

"attempt - no catch" {
checkAll(Arb.int(), Arb.long()) { i, l ->
eagerEffect<String, Int> {
attempt<Long, Int> {
i
} catch { ll ->
ll shouldBe l
i + 1
}
}.runCont() shouldBe i
}
}

"immediate values" { eagerEffect<Nothing, Int> { 1 }.toEither().orNull() shouldBe 1 }

"immediate short-circuit" { eagerEffect<String, Nothing> { shift("hello") }.runCont() shouldBe "hello" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.boolean
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.long
import io.kotest.property.arbitrary.orNull
import io.kotest.property.arbitrary.string
import io.kotest.property.checkAll
Expand Down Expand Up @@ -109,6 +110,32 @@ class EffectSpec :
}
}

"attempt - catch" {
checkAll(Arb.int(), Arb.long()) { i, l ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add these tests for EagerEffect too? I think we’ll run into the issue that shift cannot be called from the EagerEffect catch method, but I’m not 100% sure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we're able to use shift since attempt is within a suspend EagerEffectScope.() -> A ☺️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @serras! I was wrong 😁

effect<String, Int> {
attempt<Long, Int> {
shift(l)
} catch { ll ->
ll shouldBe l
i
}
}.runCont() shouldBe i
}
}

"attempt - no catch" {
checkAll(Arb.int(), Arb.long()) { i, l ->
effect<String, Int> {
attempt<Long, Int> {
i
} catch { ll ->
ll shouldBe l
i + 1
}
}.runCont() shouldBe i
}
}

"eagerEffect can be consumed within an Effect computation" {
checkAll(Arb.int(), Arb.int()) { a, b ->
val eager: EagerEffect<String, Int> =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
// This file was automatically generated from EagerEffectScope.kt by Knit tool. Do not edit.
package arrow.core.examples.exampleEagerEffectScope08

import arrow.core.Either
import arrow.core.None
import arrow.core.Option
import arrow.core.Validated
import arrow.core.continuations.eagerEffect
import arrow.core.continuations.ensureNotNull
import arrow.core.left
import arrow.core.right
import io.kotest.assertions.fail
import io.kotest.matchers.shouldBe

fun main() {
val failure = "failed"
val int: Int? = null
eagerEffect<String, Int> {
ensureNotNull(int) { failure }
}.toEither() shouldBe (int?.right() ?: failure.left())
val x = Either.Right(1).bind()
val y = Validated.Valid(2).bind()
val z =
attempt { None.bind { "Option was empty" } } catch { 0 }
x + y + z
}.fold({ fail("Shift can never be the result") }, { it shouldBe 3 })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This file was automatically generated from EagerEffectScope.kt by Knit tool. Do not edit.
package arrow.core.examples.exampleEagerEffectScope09

import arrow.core.continuations.eagerEffect
import arrow.core.continuations.ensureNotNull
import arrow.core.left
import arrow.core.right
import io.kotest.matchers.shouldBe

fun main() {
val failure = "failed"
val int: Int? = null
eagerEffect<String, Int> {
ensureNotNull(int) { failure }
}.toEither() shouldBe (int?.right() ?: failure.left())
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit.
package arrow.core.examples.exampleEffectScope09

import arrow.core.Either
import arrow.core.None
import arrow.core.Option
import arrow.core.Validated
import arrow.core.continuations.effect
import arrow.core.continuations.ensureNotNull
import arrow.core.left
import arrow.core.right
import io.kotest.assertions.fail
import io.kotest.matchers.shouldBe

suspend fun main() {
val failure = "failed"
val int: Int? = null
effect<String, Int> {
ensureNotNull(int) { failure }
}.toEither() shouldBe (int?.right() ?: failure.left())
val x = Either.Right(1).bind()
val y = Validated.Valid(2).bind()
val z =
attempt { None.bind { "Option was empty" } } catch { 0 }
x + y + z
}.fold({ fail("Shift can never be the result") }, { it shouldBe 3 })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This file was automatically generated from EffectScope.kt by Knit tool. Do not edit.
package arrow.core.examples.exampleEffectScope10

import arrow.core.continuations.effect
import arrow.core.continuations.ensureNotNull
import arrow.core.left
import arrow.core.right
import io.kotest.matchers.shouldBe

suspend fun main() {
val failure = "failed"
val int: Int? = null
effect<String, Int> {
ensureNotNull(int) { failure }
}.toEither() shouldBe (int?.right() ?: failure.left())
}