From 7701dbe77925593dc34f29a7cf68dcac21eef25b Mon Sep 17 00:00:00 2001 From: Michael Bull Date: Sun, 10 Mar 2024 23:50:14 +0000 Subject: [PATCH] Deprecate suspending variant of "binding" in favour of "coroutineBinding" This matches the internally-called function named coroutineScope, and helps consumers distinguish between the blocking variant that is otherwise only differing in package name. This should also help convey to readers that structured concurrency will occur within the block. --- ...chmark.kt => CoroutineBindingBenchmark.kt} | 4 +- .../result/coroutines/CoroutineBinding.kt | 70 ++++++++++++++++++ .../coroutines/binding/SuspendableBinding.kt | 74 +++---------------- ...ngTest.kt => AsyncCoroutineBindingTest.kt} | 12 +-- ...BindingTest.kt => CoroutineBindingTest.kt} | 14 ++-- 5 files changed, 95 insertions(+), 79 deletions(-) rename benchmarks/src/jvmMain/kotlin/com/github/michaelbull/result/{SuspendBindingBenchmark.kt => CoroutineBindingBenchmark.kt} (94%) create mode 100644 kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/CoroutineBinding.kt rename kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/{binding/AsyncSuspendableBindingTest.kt => AsyncCoroutineBindingTest.kt} (92%) rename kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/{binding/SuspendableBindingTest.kt => CoroutineBindingTest.kt} (90%) diff --git a/benchmarks/src/jvmMain/kotlin/com/github/michaelbull/result/SuspendBindingBenchmark.kt b/benchmarks/src/jvmMain/kotlin/com/github/michaelbull/result/CoroutineBindingBenchmark.kt similarity index 94% rename from benchmarks/src/jvmMain/kotlin/com/github/michaelbull/result/SuspendBindingBenchmark.kt rename to benchmarks/src/jvmMain/kotlin/com/github/michaelbull/result/CoroutineBindingBenchmark.kt index 9a0d1b0..54ed87c 100644 --- a/benchmarks/src/jvmMain/kotlin/com/github/michaelbull/result/SuspendBindingBenchmark.kt +++ b/benchmarks/src/jvmMain/kotlin/com/github/michaelbull/result/CoroutineBindingBenchmark.kt @@ -1,5 +1,6 @@ package com.github.michaelbull.result +import com.github.michaelbull.result.coroutines.coroutineBinding import kotlinx.benchmark.Benchmark import kotlinx.benchmark.BenchmarkMode import kotlinx.benchmark.BenchmarkTimeUnit @@ -11,12 +12,11 @@ import kotlinx.benchmark.State import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking -import com.github.michaelbull.result.coroutines.binding.binding as coroutineBinding @State(Scope.Benchmark) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(BenchmarkTimeUnit.MILLISECONDS) -class SuspendBindingBenchmark { +class CoroutineBindingBenchmark { @Benchmark fun nonSuspendableBinding(blackhole: Blackhole) { diff --git a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/CoroutineBinding.kt b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/CoroutineBinding.kt new file mode 100644 index 0000000..71ebfa9 --- /dev/null +++ b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/CoroutineBinding.kt @@ -0,0 +1,70 @@ +package com.github.michaelbull.result.coroutines + +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.coroutines.CoroutineContext + +/** + * Suspending variant of [binding][com.github.michaelbull.result.binding]. + * The suspendable [block] runs in a new [CoroutineScope], inheriting the parent [CoroutineContext]. + * This new scope is [cancelled][CoroutineScope.cancel] once a failing bind is encountered, eagerly cancelling all + * child [jobs][Job]. + */ +public suspend inline fun coroutineBinding(crossinline block: suspend CoroutineBindingScope.() -> V): Result { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + + lateinit var receiver: CoroutineBindingScopeImpl + + return try { + coroutineScope { + receiver = CoroutineBindingScopeImpl(this) + + with(receiver) { + Ok(block()) + } + } + } catch (ex: BindCancellationException) { + receiver.result!! + } +} + +internal object BindCancellationException : CancellationException(null as String?) + +public interface CoroutineBindingScope : CoroutineScope { + public suspend fun Result.bind(): V +} + +@PublishedApi +internal class CoroutineBindingScopeImpl( + delegate: CoroutineScope, +) : CoroutineBindingScope, CoroutineScope by delegate { + + private val mutex = Mutex() + var result: Result? = null + + override suspend fun Result.bind(): V { + return when (this) { + is Ok -> value + is Err -> mutex.withLock { + if (result == null) { + result = this + coroutineContext.cancel(BindCancellationException) + } + + throw BindCancellationException + } + } + } +} diff --git a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/binding/SuspendableBinding.kt b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/binding/SuspendableBinding.kt index e366d9f..ed61b42 100644 --- a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/binding/SuspendableBinding.kt +++ b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/binding/SuspendableBinding.kt @@ -1,76 +1,22 @@ package com.github.michaelbull.result.coroutines.binding -import com.github.michaelbull.result.Err -import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.coroutines.CoroutineContext +import com.github.michaelbull.result.coroutines.CoroutineBindingScope +import com.github.michaelbull.result.coroutines.coroutineBinding -/** - * Suspending variant of [binding][com.github.michaelbull.result.binding]. - * The suspendable [block] runs in a new [CoroutineScope], inheriting the parent [CoroutineContext]. - * This new scope is [cancelled][CoroutineScope.cancel] once a failing bind is encountered, eagerly cancelling all - * child [jobs][Job]. - */ +@Deprecated( + message = "Use coroutineBinding instead", + replaceWith = ReplaceWith( + expression = "coroutineBinding(block)", + imports = ["com.github.michaelbull.result.coroutines.coroutineBinding"] + ) +) public suspend inline fun binding(crossinline block: suspend CoroutineBindingScope.() -> V): Result { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - - lateinit var receiver: CoroutineBindingScopeImpl - - return try { - coroutineScope { - receiver = CoroutineBindingScopeImpl(this) - - with(receiver) { - Ok(block()) - } - } - } catch (ex: BindCancellationException) { - receiver.result!! - } + return coroutineBinding(block) } -internal object BindCancellationException : CancellationException(null as String?) - @Deprecated( message = "Use CoroutineBindingScope instead", replaceWith = ReplaceWith("CoroutineBindingScope") ) public typealias SuspendableResultBinding = CoroutineBindingScope - -public interface CoroutineBindingScope : CoroutineScope { - public suspend fun Result.bind(): V -} - -@PublishedApi -internal class CoroutineBindingScopeImpl( - delegate: CoroutineScope, -) : CoroutineBindingScope, CoroutineScope by delegate { - - private val mutex = Mutex() - var result: Result? = null - - override suspend fun Result.bind(): V { - return when (this) { - is Ok -> value - is Err -> mutex.withLock { - if (result == null) { - result = this - coroutineContext.cancel(BindCancellationException) - } - - throw BindCancellationException - } - } - } -} diff --git a/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/binding/AsyncSuspendableBindingTest.kt b/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/AsyncCoroutineBindingTest.kt similarity index 92% rename from kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/binding/AsyncSuspendableBindingTest.kt rename to kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/AsyncCoroutineBindingTest.kt index 866f6ff..e32c464 100644 --- a/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/binding/AsyncSuspendableBindingTest.kt +++ b/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/AsyncCoroutineBindingTest.kt @@ -1,4 +1,4 @@ -package com.github.michaelbull.result.coroutines.binding +package com.github.michaelbull.result.coroutines import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok @@ -15,7 +15,7 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue @ExperimentalCoroutinesApi -class AsyncSuspendableBindingTest { +class AsyncCoroutineBindingTest { private sealed interface BindingError { data object BindingErrorA : BindingError @@ -34,7 +34,7 @@ class AsyncSuspendableBindingTest { return Ok(2) } - val result: Result = binding { + val result: Result = coroutineBinding { val x = async { provideX().bind() } val y = async { provideY().bind() } x.await() + y.await() @@ -63,7 +63,7 @@ class AsyncSuspendableBindingTest { return Err(BindingError.BindingErrorB) } - val result: Result = binding { + val result: Result = coroutineBinding { val x = async { provideX().bind() } val y = async { provideY().bind() } val z = async { provideZ().bind() } @@ -96,7 +96,7 @@ class AsyncSuspendableBindingTest { val dispatcherA = StandardTestDispatcher(testScheduler) val dispatcherB = StandardTestDispatcher(testScheduler) - val result: Result = binding { + val result: Result = coroutineBinding { val x = async(dispatcherA) { provideX().bind() } val y = async(dispatcherB) { provideY().bind() } @@ -143,7 +143,7 @@ class AsyncSuspendableBindingTest { val dispatcherB = StandardTestDispatcher(testScheduler) val dispatcherC = StandardTestDispatcher(testScheduler) - val result: Result = binding { + val result: Result = coroutineBinding { launch(dispatcherA) { provideX().bind() } testScheduler.advanceTimeBy(20) diff --git a/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/binding/SuspendableBindingTest.kt b/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/CoroutineBindingTest.kt similarity index 90% rename from kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/binding/SuspendableBindingTest.kt rename to kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/CoroutineBindingTest.kt index 5ccdae0..8715a3d 100644 --- a/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/binding/SuspendableBindingTest.kt +++ b/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/CoroutineBindingTest.kt @@ -1,4 +1,4 @@ -package com.github.michaelbull.result.coroutines.binding +package com.github.michaelbull.result.coroutines import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok @@ -12,7 +12,7 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue @ExperimentalCoroutinesApi -class SuspendableBindingTest { +class CoroutineBindingTest { private object BindingError @@ -28,7 +28,7 @@ class SuspendableBindingTest { return Ok(2) } - val result: Result = binding { + val result: Result = coroutineBinding { val x = provideX().bind() val y = provideY().bind() x + y @@ -52,7 +52,7 @@ class SuspendableBindingTest { return Ok(x + 2) } - val result: Result = binding { + val result: Result = coroutineBinding { val x = provideX().bind() val y = provideY(x.toInt()).bind() y @@ -81,7 +81,7 @@ class SuspendableBindingTest { return Ok(2) } - val result: Result = binding { + val result: Result = coroutineBinding { val x = provideX().bind() val y = provideY().bind() val z = provideZ().bind() @@ -118,7 +118,7 @@ class SuspendableBindingTest { return Err(BindingError) } - val result: Result = binding { + val result: Result = coroutineBinding { val x = provideX().bind() val y = provideY().bind() val z = provideZ().bind() @@ -152,7 +152,7 @@ class SuspendableBindingTest { return Ok(2) } - val result: Result = binding { + val result: Result = coroutineBinding { val x = provideX().bind() val y = provideY().bind() val z = provideZ().bind()