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