Skip to content

Commit

Permalink
catch for EffectScope (#2746)
Browse files Browse the repository at this point in the history
Co-authored-by: Imran Malic Settuba <46971368+i-walker@users.noreply.github.com>
Co-authored-by: serras <serras@users.noreply.github.com>
Co-authored-by: Imran Malic Settuba <46971368+i-walker@users.noreply.github.com>
Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>
  • Loading branch information
4 people committed Jul 21, 2022
1 parent ac81bbf commit 0c50804
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 16 deletions.
36 changes: 36 additions & 0 deletions arrow-libs/core/arrow-core/api/arrow-core.api

Large diffs are not rendered by default.

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(
@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,
): 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())
}

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())
}

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
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 ->
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
@@ -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 })
}
@@ -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())
}
@@ -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 })
}
@@ -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())
}

0 comments on commit 0c50804

Please sign in to comment.