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 package com.github.michaelbull.result
import com.github.michaelbull.result.coroutines.coroutineBinding
import kotlinx.benchmark.Benchmark import kotlinx.benchmark.Benchmark
import kotlinx.benchmark.BenchmarkMode import kotlinx.benchmark.BenchmarkMode
import kotlinx.benchmark.BenchmarkTimeUnit import kotlinx.benchmark.BenchmarkTimeUnit
@ -11,12 +12,11 @@ import kotlinx.benchmark.State
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import com.github.michaelbull.result.coroutines.binding.binding as coroutineBinding
@State(Scope.Benchmark) @State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime) @BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(BenchmarkTimeUnit.MILLISECONDS) @OutputTimeUnit(BenchmarkTimeUnit.MILLISECONDS)
class SuspendBindingBenchmark { class CoroutineBindingBenchmark {
@Benchmark @Benchmark
fun nonSuspendableBinding(blackhole: Blackhole) { 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 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 com.github.michaelbull.result.Result
import kotlinx.coroutines.CancellationException import com.github.michaelbull.result.coroutines.CoroutineBindingScope
import kotlinx.coroutines.CoroutineScope import com.github.michaelbull.result.coroutines.coroutineBinding
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
/** @Deprecated(
* Suspending variant of [binding][com.github.michaelbull.result.binding]. message = "Use coroutineBinding instead",
* The suspendable [block] runs in a new [CoroutineScope], inheriting the parent [CoroutineContext]. replaceWith = ReplaceWith(
* This new scope is [cancelled][CoroutineScope.cancel] once a failing bind is encountered, eagerly cancelling all expression = "coroutineBinding(block)",
* child [jobs][Job]. imports = ["com.github.michaelbull.result.coroutines.coroutineBinding"]
*/ )
)
public suspend inline fun <V, E> binding(crossinline block: suspend CoroutineBindingScope<E>.() -> V): Result<V, E> { public suspend inline fun <V, E> binding(crossinline block: suspend CoroutineBindingScope<E>.() -> V): Result<V, E> {
contract { return coroutineBinding(block)
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?)
@Deprecated( @Deprecated(
message = "Use CoroutineBindingScope instead", message = "Use CoroutineBindingScope instead",
replaceWith = ReplaceWith("CoroutineBindingScope<E>") replaceWith = ReplaceWith("CoroutineBindingScope<E>")
) )
public typealias SuspendableResultBinding<E> = 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.Err
import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Ok
@ -15,7 +15,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class AsyncSuspendableBindingTest { class AsyncCoroutineBindingTest {
private sealed interface BindingError { private sealed interface BindingError {
data object BindingErrorA : BindingError data object BindingErrorA : BindingError
@ -34,7 +34,7 @@ class AsyncSuspendableBindingTest {
return Ok(2) return Ok(2)
} }
val result: Result<Int, BindingError> = binding { val result: Result<Int, BindingError> = coroutineBinding {
val x = async { provideX().bind() } val x = async { provideX().bind() }
val y = async { provideY().bind() } val y = async { provideY().bind() }
x.await() + y.await() x.await() + y.await()
@ -63,7 +63,7 @@ class AsyncSuspendableBindingTest {
return Err(BindingError.BindingErrorB) return Err(BindingError.BindingErrorB)
} }
val result: Result<Int, BindingError> = binding { val result: Result<Int, BindingError> = coroutineBinding {
val x = async { provideX().bind() } val x = async { provideX().bind() }
val y = async { provideY().bind() } val y = async { provideY().bind() }
val z = async { provideZ().bind() } val z = async { provideZ().bind() }
@ -96,7 +96,7 @@ class AsyncSuspendableBindingTest {
val dispatcherA = StandardTestDispatcher(testScheduler) val dispatcherA = StandardTestDispatcher(testScheduler)
val dispatcherB = 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 x = async(dispatcherA) { provideX().bind() }
val y = async(dispatcherB) { provideY().bind() } val y = async(dispatcherB) { provideY().bind() }
@ -143,7 +143,7 @@ class AsyncSuspendableBindingTest {
val dispatcherB = StandardTestDispatcher(testScheduler) val dispatcherB = StandardTestDispatcher(testScheduler)
val dispatcherC = StandardTestDispatcher(testScheduler) val dispatcherC = StandardTestDispatcher(testScheduler)
val result: Result<Unit, BindingError> = binding { val result: Result<Unit, BindingError> = coroutineBinding {
launch(dispatcherA) { provideX().bind() } launch(dispatcherA) { provideX().bind() }
testScheduler.advanceTimeBy(20) 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.Err
import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Ok
@ -12,7 +12,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class SuspendableBindingTest { class CoroutineBindingTest {
private object BindingError private object BindingError
@ -28,7 +28,7 @@ class SuspendableBindingTest {
return Ok(2) return Ok(2)
} }
val result: Result<Int, BindingError> = binding { val result: Result<Int, BindingError> = coroutineBinding {
val x = provideX().bind() val x = provideX().bind()
val y = provideY().bind() val y = provideY().bind()
x + y x + y
@ -52,7 +52,7 @@ class SuspendableBindingTest {
return Ok(x + 2) return Ok(x + 2)
} }
val result: Result<Int, BindingError> = binding { val result: Result<Int, BindingError> = coroutineBinding {
val x = provideX().bind() val x = provideX().bind()
val y = provideY(x.toInt()).bind() val y = provideY(x.toInt()).bind()
y y
@ -81,7 +81,7 @@ class SuspendableBindingTest {
return Ok(2) return Ok(2)
} }
val result: Result<Int, BindingError> = binding { val result: Result<Int, BindingError> = coroutineBinding {
val x = provideX().bind() val x = provideX().bind()
val y = provideY().bind() val y = provideY().bind()
val z = provideZ().bind() val z = provideZ().bind()
@ -118,7 +118,7 @@ class SuspendableBindingTest {
return Err(BindingError) return Err(BindingError)
} }
val result: Result<Int, BindingError> = binding { val result: Result<Int, BindingError> = coroutineBinding {
val x = provideX().bind() val x = provideX().bind()
val y = provideY().bind() val y = provideY().bind()
val z = provideZ().bind() val z = provideZ().bind()
@ -152,7 +152,7 @@ class SuspendableBindingTest {
return Ok(2) return Ok(2)
} }
val result: Result<Int, BindingError> = binding { val result: Result<Int, BindingError> = coroutineBinding {
val x = provideX().bind() val x = provideX().bind()
val y = provideY().bind() val y = provideY().bind()
val z = provideZ().bind() val z = provideZ().bind()