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.
This commit is contained in:
Michael Bull 2024-03-10 23:50:14 +00:00
parent dd5c96f983
commit b19894a08c
5 changed files with 95 additions and 79 deletions

View File

@ -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) {

View File

@ -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 <V, E> coroutineBinding(crossinline block: suspend CoroutineBindingScope<E>.() -> V): Result<V, E> {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
lateinit var receiver: CoroutineBindingScopeImpl<E>
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<E> : CoroutineScope {
public suspend fun <V> Result<V, E>.bind(): V
}
@PublishedApi
internal class CoroutineBindingScopeImpl<E>(
delegate: CoroutineScope,
) : CoroutineBindingScope<E>, CoroutineScope by delegate {
private val mutex = Mutex()
var result: Result<Nothing, E>? = null
override suspend fun <V> Result<V, E>.bind(): V {
return when (this) {
is Ok -> value
is Err -> mutex.withLock {
if (result == null) {
result = this
coroutineContext.cancel(BindCancellationException)
}
throw BindCancellationException
}
}
}
}

View File

@ -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 <V, E> binding(crossinline block: suspend CoroutineBindingScope<E>.() -> V): Result<V, E> {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
return coroutineBinding(block)
}
lateinit var receiver: CoroutineBindingScopeImpl<E>
return try {
coroutineScope {
receiver = CoroutineBindingScopeImpl(this)
with(receiver) {
Ok(block())
}
}
} catch (ex: BindCancellationException) {
receiver.result!!
}
}
internal object BindCancellationException : CancellationException(null as String?)
@Deprecated(
message = "Use CoroutineBindingScope instead",
replaceWith = ReplaceWith("CoroutineBindingScope<E>")
)
public typealias SuspendableResultBinding<E> = CoroutineBindingScope<E>
public interface CoroutineBindingScope<E> : CoroutineScope {
public suspend fun <V> Result<V, E>.bind(): V
}
@PublishedApi
internal class CoroutineBindingScopeImpl<E>(
delegate: CoroutineScope,
) : CoroutineBindingScope<E>, CoroutineScope by delegate {
private val mutex = Mutex()
var result: Result<Nothing, E>? = null
override suspend fun <V> Result<V, E>.bind(): V {
return when (this) {
is Ok -> value
is Err -> mutex.withLock {
if (result == null) {
result = this
coroutineContext.cancel(BindCancellationException)
}
throw BindCancellationException
}
}
}
}

View File

@ -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<Int, BindingError> = binding {
val result: Result<Int, BindingError> = 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<Int, BindingError> = binding {
val result: Result<Int, BindingError> = 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<Int, BindingError> = binding {
val result: Result<Int, BindingError> = 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<Unit, BindingError> = binding {
val result: Result<Unit, BindingError> = coroutineBinding {
launch(dispatcherA) { provideX().bind() }
testScheduler.advanceTimeBy(20)

View File

@ -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<Int, BindingError> = binding {
val result: Result<Int, BindingError> = coroutineBinding {
val x = provideX().bind()
val y = provideY().bind()
x + y
@ -52,7 +52,7 @@ class SuspendableBindingTest {
return Ok(x + 2)
}
val result: Result<Int, BindingError> = binding {
val result: Result<Int, BindingError> = coroutineBinding {
val x = provideX().bind()
val y = provideY(x.toInt()).bind()
y
@ -81,7 +81,7 @@ class SuspendableBindingTest {
return Ok(2)
}
val result: Result<Int, BindingError> = binding {
val result: Result<Int, BindingError> = 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<Int, BindingError> = binding {
val result: Result<Int, BindingError> = 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<Int, BindingError> = binding {
val result: Result<Int, BindingError> = coroutineBinding {
val x = provideX().bind()
val y = provideY().bind()
val z = provideZ().bind()