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 e0bbf54beb5..4152a05e116 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 @@ -568,7 +568,7 @@ import kotlin.coroutines.resume * ``` * */ -public interface Effect { +public sealed interface Effect { /** * Runs the suspending computation by creating a [Continuation], and running the `fold` function * over the computation. @@ -704,6 +704,10 @@ internal class Token { * the result. */ @PublishedApi +@Deprecated( + "This will become private in Arrow 2.0, and is not going to be visible from binary anymore", + level = DeprecationLevel.WARNING +) internal class FoldContinuation( private val token: Token, override val context: CoroutineContext, @@ -754,7 +758,11 @@ internal class FoldContinuation( */ public fun effect(f: suspend EffectScope.() -> A): Effect = DefaultEffect(f) -private class DefaultEffect(private val f: suspend EffectScope.() -> A) : Effect { +@Deprecated( + "This will be removed in Arrow 2.0", + level = DeprecationLevel.WARNING +) +internal 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 = diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EffectScope.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EffectScope.kt index 5a1f7d23d6d..60e9332711d 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EffectScope.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/continuations/EffectScope.kt @@ -54,7 +54,10 @@ public interface EffectScope { * ``` * */ - public suspend fun Effect.bind(): B = fold(this@EffectScope::shift, ::identity) + public suspend fun Effect.bind(): B = + when (this) { + is DefaultEffect -> f(this@EffectScope) + } /** * Runs the [EagerEffect] to finish, returning [B] or [shift] in case of [R], 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 f5566949003..4bc582ee6e2 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 @@ -21,6 +21,7 @@ import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn import kotlin.coroutines.startCoroutine import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class EffectSpec : StringSpec({ @@ -241,8 +242,27 @@ class EffectSpec : }.runCont() } shouldBe Either.Left(e) } + + "#2760 - dispatching in nested Effect blocks does not make the nested Continuation to hang" { + checkAll(Arb.string()) { msg -> + fun failure(): Effect = effect { + withContext(Dispatchers.Default) {} + shift(Failure(msg)) + } + + effect { + failure().bind() + 1 + }.fold( + recover = { it }, + transform = { fail("Should never come here") }, + ) shouldBe Failure(msg) + } + } }) +private data class Failure(val msg: String) + suspend fun currentContext(): CoroutineContext = kotlin.coroutines.coroutineContext internal suspend fun Throwable.suspend(): Nothing = suspendCoroutineUninterceptedOrReturn { cont ->