Skip to content

Commit

Permalink
Deprecate suspending variant of "binding" in favour of "coroutineBind…
Browse files Browse the repository at this point in the history
…ing"

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.
  • Loading branch information
michaelbull committed Mar 11, 2024
1 parent dd5c96f commit b19894a
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}

lateinit var receiver: CoroutineBindingScopeImpl<E>

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<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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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() }
Expand Down Expand Up @@ -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() }

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,7 +12,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue

@ExperimentalCoroutinesApi
class SuspendableBindingTest {
class CoroutineBindingTest {

private object BindingError

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit b19894a

Please sign in to comment.