From 23b97d564f2fa8d8b1018483381eec05306f1c9a Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Thu, 4 Aug 2022 13:18:17 +0200 Subject: [PATCH 1/7] Fix usage of startCoroutineUninterceptedOrReturn --- .../kotlin/arrow/core/continuations/Effect.kt | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt index a66f6563786..db472688b30 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt @@ -11,10 +11,11 @@ import arrow.core.nonFatalOrThrow import kotlin.coroutines.Continuation import kotlin.coroutines.CoroutineContext import kotlin.coroutines.cancellation.CancellationException -import kotlin.coroutines.intrinsics.createCoroutineUnintercepted +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException /** * [Effect] represents a function of `suspend () -> A`, that short-circuit with a value of [R] (and [Throwable]), @@ -596,9 +597,9 @@ public interface Effect { */ public suspend fun fold( recover: suspend (shifted: R) -> B, - transform: suspend (value: A) -> B + transform: suspend (value: A) -> B, ): B - + /** * Like [fold] but also allows folding over any unexpected [Throwable] that might have occurred. * @see fold @@ -606,45 +607,45 @@ public interface Effect { public suspend fun fold( error: suspend (error: Throwable) -> B, recover: suspend (shifted: R) -> B, - transform: suspend (value: A) -> B + transform: suspend (value: A) -> B, ): B = try { fold(recover, transform) } catch (e: Throwable) { error(e.nonFatalOrThrow()) } - + /** * [fold] the [Effect] into an [Either]. Where the shifted value [R] is mapped to [Either.Left], and * result value [A] is mapped to [Either.Right]. */ public suspend fun toEither(): Either = fold({ Either.Left(it) }) { Either.Right(it) } - + /** * [fold] the [Effect] into an [Ior]. Where the shifted value [R] is mapped to [Ior.Left], and * result value [A] is mapped to [Ior.Right]. */ public suspend fun toIor(): Ior = fold({ Ior.Left(it) }) { Ior.Right(it) } - + /** * [fold] the [Effect] into an [Validated]. Where the shifted value [R] is mapped to * [Validated.Invalid], and result value [A] is mapped to [Validated.Valid]. */ public suspend fun toValidated(): Validated = fold({ Validated.Invalid(it) }) { Validated.Valid(it) } - + /** * [fold] the [Effect] into an [Option]. Where the shifted value [R] is mapped to [Option] by the * provided function [orElse], and result value [A] is mapped to [Some]. */ public suspend fun toOption(orElse: suspend (R) -> Option<@UnsafeVariance A>): Option = fold(orElse, ::Some) - + /** * [fold] the [Effect] into an [A?]. Where the shifted value [R] is mapped to * [null], and result value [A]. */ public suspend fun orNull(): A? = fold({ null }, ::identity) - + /** Runs the [Effect] and captures any [NonFatal] exception into [Result]. */ public fun attempt(): Effect> = effect { try { @@ -653,23 +654,23 @@ public interface Effect { Result.failure(e.nonFatalOrThrow()) } } - + public fun handleError(recover: suspend (R) -> @UnsafeVariance A): Effect = effect { fold(recover, ::identity) } - + public fun handleErrorWith(recover: suspend (R) -> Effect): Effect = effect { fold({ recover(it).bind() }, ::identity) } - + public fun redeem(recover: suspend (R) -> B, transform: suspend (A) -> B): Effect = effect { fold(recover, transform) } - + public fun redeemWith( recover: suspend (R) -> Effect, - transform: suspend (A) -> Effect + transform: suspend (A) -> Effect, ): Effect = effect { fold(recover, transform).bind() } } @@ -717,7 +718,14 @@ internal class FoldContinuation( result.fold(parent::resume) { throwable -> if (throwable is Suspend && token == throwable.token) { val f: suspend () -> B = { throwable.recover(throwable.shifted) as B } - f.createCoroutineUnintercepted(parent).resume(Unit) + try { + when (val res = f.startCoroutineUninterceptedOrReturn(parent)) { + COROUTINE_SUSPENDED -> Unit + else -> parent.resume(res as B) + } + } catch (e: Throwable) { + parent.resumeWithException(e) + } } else parent.resumeWith(result) } } @@ -777,7 +785,7 @@ private class DefaultEffect(val f: suspend EffectScope.() -> A) : Effec // See: EffectSpec - try/catch tests throw Suspend(token, r, recover as suspend (Any?) -> Any?) } - + try { suspend { transform(f(effectScope)) } .startCoroutineUninterceptedOrReturn(FoldContinuation(token, cont.context, cont)) From 719261a2992010ed1a5508bbd8db8e8f1404ac18 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Thu, 4 Aug 2022 13:28:37 +0200 Subject: [PATCH 2/7] Revert unrelated changes --- .../kotlin/arrow/core/continuations/Effect.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt index db472688b30..77ea4832745 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt @@ -597,9 +597,9 @@ public interface Effect { */ public suspend fun fold( recover: suspend (shifted: R) -> B, - transform: suspend (value: A) -> B, + transform: suspend (value: A) -> B ): B - + /** * Like [fold] but also allows folding over any unexpected [Throwable] that might have occurred. * @see fold @@ -607,45 +607,45 @@ public interface Effect { public suspend fun fold( error: suspend (error: Throwable) -> B, recover: suspend (shifted: R) -> B, - transform: suspend (value: A) -> B, + transform: suspend (value: A) -> B ): B = try { fold(recover, transform) } catch (e: Throwable) { error(e.nonFatalOrThrow()) } - + /** * [fold] the [Effect] into an [Either]. Where the shifted value [R] is mapped to [Either.Left], and * result value [A] is mapped to [Either.Right]. */ public suspend fun toEither(): Either = fold({ Either.Left(it) }) { Either.Right(it) } - + /** * [fold] the [Effect] into an [Ior]. Where the shifted value [R] is mapped to [Ior.Left], and * result value [A] is mapped to [Ior.Right]. */ public suspend fun toIor(): Ior = fold({ Ior.Left(it) }) { Ior.Right(it) } - + /** * [fold] the [Effect] into an [Validated]. Where the shifted value [R] is mapped to * [Validated.Invalid], and result value [A] is mapped to [Validated.Valid]. */ public suspend fun toValidated(): Validated = fold({ Validated.Invalid(it) }) { Validated.Valid(it) } - + /** * [fold] the [Effect] into an [Option]. Where the shifted value [R] is mapped to [Option] by the * provided function [orElse], and result value [A] is mapped to [Some]. */ public suspend fun toOption(orElse: suspend (R) -> Option<@UnsafeVariance A>): Option = fold(orElse, ::Some) - + /** * [fold] the [Effect] into an [A?]. Where the shifted value [R] is mapped to * [null], and result value [A]. */ public suspend fun orNull(): A? = fold({ null }, ::identity) - + /** Runs the [Effect] and captures any [NonFatal] exception into [Result]. */ public fun attempt(): Effect> = effect { try { @@ -654,23 +654,23 @@ public interface Effect { Result.failure(e.nonFatalOrThrow()) } } - + public fun handleError(recover: suspend (R) -> @UnsafeVariance A): Effect = effect { fold(recover, ::identity) } - + public fun handleErrorWith(recover: suspend (R) -> Effect): Effect = effect { fold({ recover(it).bind() }, ::identity) } - + public fun redeem(recover: suspend (R) -> B, transform: suspend (A) -> B): Effect = effect { fold(recover, transform) } - + public fun redeemWith( recover: suspend (R) -> Effect, - transform: suspend (A) -> Effect, + transform: suspend (A) -> Effect ): Effect = effect { fold(recover, transform).bind() } } @@ -785,7 +785,7 @@ private class DefaultEffect(val f: suspend EffectScope.() -> A) : Effec // See: EffectSpec - try/catch tests throw Suspend(token, r, recover as suspend (Any?) -> Any?) } - + try { suspend { transform(f(effectScope)) } .startCoroutineUninterceptedOrReturn(FoldContinuation(token, cont.context, cont)) From 0481bc116ce8ecfa721362d144bf7c3d231dd9b7 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Thu, 4 Aug 2022 17:23:00 +0200 Subject: [PATCH 3/7] Fix Throwable path --- .../kotlin/arrow/core/continuations/Effect.kt | 58 +++++++++++-------- .../arrow/core/continuations/EffectSpec.kt | 24 +++++++- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt index 77ea4832745..893c8ef7807 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt @@ -598,7 +598,7 @@ public interface Effect { public suspend fun fold( recover: suspend (shifted: R) -> B, transform: suspend (value: A) -> B - ): B + ): B = fold({ throw it }, recover, transform) /** * Like [fold] but also allows folding over any unexpected [Throwable] that might have occurred. @@ -607,14 +607,9 @@ public interface Effect { public suspend fun fold( error: suspend (error: Throwable) -> B, recover: suspend (shifted: R) -> B, - transform: suspend (value: A) -> B - ): B = - try { - fold(recover, transform) - } catch (e: Throwable) { - error(e.nonFatalOrThrow()) - } - + transform: suspend (value: A) -> B, + ): B + /** * [fold] the [Effect] into an [Either]. Where the shifted value [R] is mapped to [Either.Left], and * result value [A] is mapped to [Either.Right]. @@ -712,21 +707,31 @@ internal class Token { internal class FoldContinuation( private val token: Token, override val context: CoroutineContext, - private val parent: Continuation + private val error: suspend (Throwable) -> B, + private val parent: Continuation, ) : Continuation { + // In contrast to `createCoroutineUnintercepted this doesn't create a new ContinuationImpl + private fun (suspend () -> A).startCoroutineUnintercepted(cont: Continuation): Unit { + try { + when (val res = startCoroutineUninterceptedOrReturn(cont)) { + COROUTINE_SUSPENDED -> Unit + else -> cont.resume(res as A) + } + // We need to wire all immediately throw exceptions to the parent Continuation + } catch (e: Throwable) { + cont.resumeWithException(e) + } + } + override fun resumeWith(result: Result) { result.fold(parent::resume) { throwable -> - if (throwable is Suspend && token == throwable.token) { - val f: suspend () -> B = { throwable.recover(throwable.shifted) as B } - try { - when (val res = f.startCoroutineUninterceptedOrReturn(parent)) { - COROUTINE_SUSPENDED -> Unit - else -> parent.resume(res as B) - } - } catch (e: Throwable) { - parent.resumeWithException(e) - } - } else parent.resumeWith(result) + when { + throwable is Suspend && token == throwable.token -> + suspend { throwable.recover(throwable.shifted) as B }.startCoroutineUnintercepted(parent) + + throwable !is Suspend -> suspend { error(throwable) }.startCoroutineUnintercepted(parent) + else -> parent.resumeWith(result) + } } } } @@ -766,7 +771,11 @@ public fun effect(f: suspend EffectScope.() -> A): Effect = Defa private class DefaultEffect(val f: suspend EffectScope.() -> A) : Effect { // We create a `Token` for fold Continuation, so we can properly differentiate between nested // folds - override suspend fun fold(recover: suspend (R) -> B, transform: suspend (A) -> B): B = + override suspend fun fold( + error: suspend (error: Throwable) -> B, + recover: suspend (shifted: R) -> B, + transform: suspend (value: A) -> B, + ): B = suspendCoroutineUninterceptedOrReturn { cont -> val token = Token() val effectScope = @@ -788,12 +797,15 @@ private class DefaultEffect(val f: suspend EffectScope.() -> A) : Effec try { suspend { transform(f(effectScope)) } - .startCoroutineUninterceptedOrReturn(FoldContinuation(token, cont.context, cont)) + .startCoroutineUninterceptedOrReturn(FoldContinuation(token, cont.context, error, cont)) } catch (e: Suspend) { if (token == e.token) { val f: suspend () -> B = { e.recover(e.shifted) as B } f.startCoroutineUninterceptedOrReturn(cont) } else throw e + } catch (e: Throwable) { + val f: suspend () -> B = { error(e.nonFatalOrThrow()) } + f.startCoroutineUninterceptedOrReturn(cont) } } } diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt index 81f62f911e1..d5db6fc1f1c 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt @@ -5,11 +5,12 @@ import arrow.core.identity import arrow.core.left import arrow.core.right import io.kotest.assertions.fail -import io.kotest.common.runBlocking import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.kotest.property.Arb +import io.kotest.property.arbitrary.arbitrary import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.flatMap import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.long import io.kotest.property.arbitrary.orNull @@ -323,12 +324,33 @@ class EffectSpec : newError.toEither() shouldBe Either.Left(error.reversed().toList()) } } + + "Can handle thrown exceptions" { + checkAll(Arb.string().suspend(), Arb.string().suspend()) { msg, fallback -> + effect { + throw RuntimeException(msg()) + }.fold( + { fallback() }, + ::identity, + ::identity + ) shouldBe fallback() + } + } }) private data class Failure(val msg: String) suspend fun currentContext(): CoroutineContext = kotlin.coroutines.coroutineContext +// Turn `A` into `suspend () -> A` which tests both the `immediate` and `COROUTINE_SUSPENDED` path. +private fun Arb.suspend(): Arb A> = + flatMap { a -> + arbitrary(listOf( + { a }, + suspend { a.suspend() } + )) { suspend { a.suspend() } } + } + internal suspend fun Throwable.suspend(): Nothing = suspendCoroutineUninterceptedOrReturn { cont -> suspend { throw this } .startCoroutine(Continuation(Dispatchers.Default) { cont.intercepted().resumeWith(it) }) From bf932122bdf121a1d9a4c7d078825068f9a331a3 Mon Sep 17 00:00:00 2001 From: nomisRev Date: Thu, 4 Aug 2022 15:27:01 +0000 Subject: [PATCH 4/7] Update API files --- arrow-libs/core/arrow-core/api/arrow-core.api | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow-libs/core/arrow-core/api/arrow-core.api b/arrow-libs/core/arrow-core/api/arrow-core.api index fc33b4f3d0d..811a6a82765 100644 --- a/arrow-libs/core/arrow-core/api/arrow-core.api +++ b/arrow-libs/core/arrow-core/api/arrow-core.api @@ -2674,7 +2674,7 @@ public abstract interface class arrow/core/continuations/Effect { public final class arrow/core/continuations/Effect$DefaultImpls { public static fun attempt (Larrow/core/continuations/Effect;)Larrow/core/continuations/Effect; - public static fun fold (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun fold (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun handleError (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; public static fun handleErrorWith (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; public static fun orNull (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -2721,7 +2721,7 @@ public final class arrow/core/continuations/EffectScopeKt { } public final class arrow/core/continuations/FoldContinuation : kotlin/coroutines/Continuation { - public fun (Larrow/core/continuations/Token;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)V + public fun (Larrow/core/continuations/Token;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)V public fun getContext ()Lkotlin/coroutines/CoroutineContext; public fun resumeWith (Ljava/lang/Object;)V } From 2a3d5f2e2c1d3a4fc0f93d55a9e71511001ecbdc Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Thu, 4 Aug 2022 19:03:45 +0200 Subject: [PATCH 5/7] Retrigger CI after API files From 567790bc0403f085c4a6ee1046169cc3ce356aa7 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Fri, 5 Aug 2022 21:12:43 +0200 Subject: [PATCH 6/7] Fix binary compat --- arrow-libs/core/arrow-core/api/arrow-core.api | 3 +- .../kotlin/arrow/core/continuations/Effect.kt | 49 ++++++++++++------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/arrow-libs/core/arrow-core/api/arrow-core.api b/arrow-libs/core/arrow-core/api/arrow-core.api index 811a6a82765..911840b8a4f 100644 --- a/arrow-libs/core/arrow-core/api/arrow-core.api +++ b/arrow-libs/core/arrow-core/api/arrow-core.api @@ -2674,7 +2674,7 @@ public abstract interface class arrow/core/continuations/Effect { public final class arrow/core/continuations/Effect$DefaultImpls { public static fun attempt (Larrow/core/continuations/Effect;)Larrow/core/continuations/Effect; - public static fun fold (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun fold (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun handleError (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; public static fun handleErrorWith (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function2;)Larrow/core/continuations/Effect; public static fun orNull (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -2721,6 +2721,7 @@ public final class arrow/core/continuations/EffectScopeKt { } public final class arrow/core/continuations/FoldContinuation : kotlin/coroutines/Continuation { + public fun (Larrow/core/continuations/Token;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)V public fun (Larrow/core/continuations/Token;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)V public fun getContext ()Lkotlin/coroutines/CoroutineContext; public fun resumeWith (Ljava/lang/Object;)V diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt index 893c8ef7807..018b27db8dc 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt @@ -597,9 +597,9 @@ public interface Effect { */ public suspend fun fold( recover: suspend (shifted: R) -> B, - transform: suspend (value: A) -> B - ): B = fold({ throw it }, recover, transform) - + transform: suspend (value: A) -> B, + ): B + /** * Like [fold] but also allows folding over any unexpected [Throwable] that might have occurred. * @see fold @@ -608,39 +608,44 @@ public interface Effect { error: suspend (error: Throwable) -> B, recover: suspend (shifted: R) -> B, transform: suspend (value: A) -> B, - ): B + ): B = + try { + fold(recover, transform) + } catch (e: Throwable) { + error(e.nonFatalOrThrow()) + } /** * [fold] the [Effect] into an [Either]. Where the shifted value [R] is mapped to [Either.Left], and * result value [A] is mapped to [Either.Right]. */ public suspend fun toEither(): Either = fold({ Either.Left(it) }) { Either.Right(it) } - + /** * [fold] the [Effect] into an [Ior]. Where the shifted value [R] is mapped to [Ior.Left], and * result value [A] is mapped to [Ior.Right]. */ public suspend fun toIor(): Ior = fold({ Ior.Left(it) }) { Ior.Right(it) } - + /** * [fold] the [Effect] into an [Validated]. Where the shifted value [R] is mapped to * [Validated.Invalid], and result value [A] is mapped to [Validated.Valid]. */ public suspend fun toValidated(): Validated = fold({ Validated.Invalid(it) }) { Validated.Valid(it) } - + /** * [fold] the [Effect] into an [Option]. Where the shifted value [R] is mapped to [Option] by the * provided function [orElse], and result value [A] is mapped to [Some]. */ public suspend fun toOption(orElse: suspend (R) -> Option<@UnsafeVariance A>): Option = fold(orElse, ::Some) - + /** * [fold] the [Effect] into an [A?]. Where the shifted value [R] is mapped to * [null], and result value [A]. */ public suspend fun orNull(): A? = fold({ null }, ::identity) - + /** Runs the [Effect] and captures any [NonFatal] exception into [Result]. */ public fun attempt(): Effect> = effect { try { @@ -649,23 +654,23 @@ public interface Effect { Result.failure(e.nonFatalOrThrow()) } } - + public fun handleError(recover: suspend (R) -> @UnsafeVariance A): Effect = effect { fold(recover, ::identity) } - + public fun handleErrorWith(recover: suspend (R) -> Effect): Effect = effect { fold({ recover(it).bind() }, ::identity) } - + public fun redeem(recover: suspend (R) -> B, transform: suspend (A) -> B): Effect = effect { fold(recover, transform) } - + public fun redeemWith( recover: suspend (R) -> Effect, - transform: suspend (A) -> Effect + transform: suspend (A) -> Effect, ): Effect = effect { fold(recover, transform).bind() } } @@ -707,9 +712,12 @@ internal class Token { internal class FoldContinuation( private val token: Token, override val context: CoroutineContext, - private val error: suspend (Throwable) -> B, + private val error: (suspend (Throwable) -> B), private val parent: Continuation, ) : Continuation { + + constructor(token: Token, context: CoroutineContext, parent: Continuation) : this(token, context, { throw it }, parent) + // In contrast to `createCoroutineUnintercepted this doesn't create a new ContinuationImpl private fun (suspend () -> A).startCoroutineUnintercepted(cont: Continuation): Unit { try { @@ -769,8 +777,13 @@ internal class FoldContinuation( public fun effect(f: suspend EffectScope.() -> A): Effect = DefaultEffect(f) private class DefaultEffect(val f: suspend EffectScope.() -> A) : Effect { - // We create a `Token` for fold Continuation, so we can properly differentiate between nested - // folds + + override suspend fun fold( + recover: suspend (shifted: R) -> B, + transform: suspend (value: A) -> B, + ): B = fold({ throw it }, recover, transform) + + // We create a `Token` for fold Continuation, so we can properly differentiate between nested folds override suspend fun fold( error: suspend (error: Throwable) -> B, recover: suspend (shifted: R) -> B, @@ -794,7 +807,7 @@ private class DefaultEffect(val f: suspend EffectScope.() -> A) : Effec // See: EffectSpec - try/catch tests throw Suspend(token, r, recover as suspend (Any?) -> Any?) } - + try { suspend { transform(f(effectScope)) } .startCoroutineUninterceptedOrReturn(FoldContinuation(token, cont.context, error, cont)) From f0bf30aff982f045d965b06645357784326554d0 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Fri, 5 Aug 2022 22:04:20 +0200 Subject: [PATCH 7/7] Clean-up --- .../kotlin/arrow/core/continuations/Effect.kt | 36 +++++++++---------- .../arrow/core/continuations/EffectSpec.kt | 29 +++++++++++++++ 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt index 018b27db8dc..73e00375f19 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/Effect.kt @@ -597,9 +597,9 @@ public interface Effect { */ public suspend fun fold( recover: suspend (shifted: R) -> B, - transform: suspend (value: A) -> B, + transform: suspend (value: A) -> B ): B - + /** * Like [fold] but also allows folding over any unexpected [Throwable] that might have occurred. * @see fold @@ -607,45 +607,45 @@ public interface Effect { public suspend fun fold( error: suspend (error: Throwable) -> B, recover: suspend (shifted: R) -> B, - transform: suspend (value: A) -> B, + transform: suspend (value: A) -> B ): B = try { fold(recover, transform) } catch (e: Throwable) { error(e.nonFatalOrThrow()) } - + /** * [fold] the [Effect] into an [Either]. Where the shifted value [R] is mapped to [Either.Left], and * result value [A] is mapped to [Either.Right]. */ public suspend fun toEither(): Either = fold({ Either.Left(it) }) { Either.Right(it) } - + /** * [fold] the [Effect] into an [Ior]. Where the shifted value [R] is mapped to [Ior.Left], and * result value [A] is mapped to [Ior.Right]. */ public suspend fun toIor(): Ior = fold({ Ior.Left(it) }) { Ior.Right(it) } - + /** * [fold] the [Effect] into an [Validated]. Where the shifted value [R] is mapped to * [Validated.Invalid], and result value [A] is mapped to [Validated.Valid]. */ public suspend fun toValidated(): Validated = fold({ Validated.Invalid(it) }) { Validated.Valid(it) } - + /** * [fold] the [Effect] into an [Option]. Where the shifted value [R] is mapped to [Option] by the * provided function [orElse], and result value [A] is mapped to [Some]. */ public suspend fun toOption(orElse: suspend (R) -> Option<@UnsafeVariance A>): Option = fold(orElse, ::Some) - + /** * [fold] the [Effect] into an [A?]. Where the shifted value [R] is mapped to * [null], and result value [A]. */ public suspend fun orNull(): A? = fold({ null }, ::identity) - + /** Runs the [Effect] and captures any [NonFatal] exception into [Result]. */ public fun attempt(): Effect> = effect { try { @@ -654,23 +654,23 @@ public interface Effect { Result.failure(e.nonFatalOrThrow()) } } - + public fun handleError(recover: suspend (R) -> @UnsafeVariance A): Effect = effect { fold(recover, ::identity) } - + public fun handleErrorWith(recover: suspend (R) -> Effect): Effect = effect { fold({ recover(it).bind() }, ::identity) } - + public fun redeem(recover: suspend (R) -> B, transform: suspend (A) -> B): Effect = effect { fold(recover, transform) } - + public fun redeemWith( recover: suspend (R) -> Effect, - transform: suspend (A) -> Effect, + transform: suspend (A) -> Effect ): Effect = effect { fold(recover, transform).bind() } } @@ -712,8 +712,8 @@ internal class Token { internal class FoldContinuation( private val token: Token, override val context: CoroutineContext, - private val error: (suspend (Throwable) -> B), - private val parent: Continuation, + private val error: suspend (Throwable) -> B, + private val parent: Continuation ) : Continuation { constructor(token: Token, context: CoroutineContext, parent: Continuation) : this(token, context, { throw it }, parent) @@ -737,7 +737,7 @@ internal class FoldContinuation( throwable is Suspend && token == throwable.token -> suspend { throwable.recover(throwable.shifted) as B }.startCoroutineUnintercepted(parent) - throwable !is Suspend -> suspend { error(throwable) }.startCoroutineUnintercepted(parent) + throwable !is Suspend -> suspend { error(throwable.nonFatalOrThrow()) }.startCoroutineUnintercepted(parent) else -> parent.resumeWith(result) } } @@ -807,7 +807,7 @@ private class DefaultEffect(val f: suspend EffectScope.() -> A) : Effec // See: EffectSpec - try/catch tests throw Suspend(token, r, recover as suspend (Any?) -> Any?) } - + try { suspend { transform(f(effectScope)) } .startCoroutineUninterceptedOrReturn(FoldContinuation(token, cont.context, error, cont)) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt index d5db6fc1f1c..af23c30c529 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/continuations/EffectSpec.kt @@ -5,6 +5,7 @@ import arrow.core.identity import arrow.core.left import arrow.core.right import io.kotest.assertions.fail +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.kotest.property.Arb @@ -336,6 +337,34 @@ class EffectSpec : ) shouldBe fallback() } } + + "Can shift from thrown exceptions" { + checkAll(Arb.string().suspend(), Arb.string().suspend()) { msg, fallback -> + effect { + effect { + throw RuntimeException(msg()) + }.fold( + { shift(fallback()) }, + ::identity, + { it.length } + ) + }.runCont() shouldBe fallback() + } + } + + "Can throw from thrown exceptions" { + checkAll(Arb.string().suspend(), Arb.string().suspend()) { msg, fallback -> + shouldThrow { + effect { + throw RuntimeException(msg()) + }.fold( + { throw IllegalStateException(fallback()) }, + ::identity, + { it.length } + ) + }.message shouldBe fallback() + } + } }) private data class Failure(val msg: String)