From 8bd343d9313c1cb9f6d771b6c66ac2d0d4ee68a0 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 20 Apr 2025 17:03:25 +0700 Subject: [PATCH 1/3] add `parZip`s --- .../michaelbull/result/coroutines/ParZip.kt | 219 ++++++++++++++++++ .../ParZipTest.kt | 45 ++++ 2 files changed, 264 insertions(+) create mode 100644 kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt create mode 100644 kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/ParZipTest.kt diff --git a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt new file mode 100644 index 0000000..b55cf82 --- /dev/null +++ b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt @@ -0,0 +1,219 @@ +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 com.github.michaelbull.result.getOrThrow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.jvm.JvmField +import kotlin.jvm.Transient + +private typealias Producer = suspend CoroutineScope.() -> Result + +@PublishedApi +internal class ParZipException( + @JvmField @Transient val error: Any? +) : RuntimeException("parZip failed with error: $error") + +@PublishedApi +internal inline val Result.valueOrThrowParZipException: V + get() = getOrThrow(::ParZipException) + + +@Suppress("UNCHECKED_CAST") +@PublishedApi +internal inline fun ParZipException.toErr(): Result = Err(error as E) + + +/** + * Runs [producer1] and [producer2] in parallel on [context], combining their successful results with [transform]. + * If either computation fails with an [Err], the other is cancelled, and the error is returned as [Err]. + * + * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument. + * If the combined context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * **WARNING** If the combined context has a single threaded [ContinuationInterceptor], + * this function will not run [producer1], [producer2] in parallel. + */ +public suspend inline fun parZip( + context: CoroutineContext = EmptyCoroutineContext, + crossinline producer1: Producer, + crossinline producer2: Producer, + crossinline transform: suspend CoroutineScope.(value1: T1, value2: T2) -> V, +): Result { + contract { + callsInPlace(producer1, InvocationKind.AT_MOST_ONCE) + callsInPlace(producer2, InvocationKind.AT_MOST_ONCE) + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + } + return try { + coroutineScope { + val d1 = async(context) { producer1().valueOrThrowParZipException } + val d2 = async(context) { producer2().valueOrThrowParZipException } + val values = awaitAll(d1, d2) + Ok( + @Suppress("UNCHECKED_CAST") + transform(values[0] as T1, values[1] as T2) + ) + } + } catch (e: ParZipException) { + e.toErr() + } +} + +/** + * Runs [producer1] and [producer2] in parallel on [Dispatchers.Default], combining their successful results with [transform]. + * If either computation fails with an [Err], the other is cancelled, and the error is returned as [Err]. + */ +public suspend inline fun parZip( + crossinline producer1: Producer, + crossinline producer2: Producer, + crossinline transform: suspend CoroutineScope.(value1: T1, value2: T2) -> V, +): Result = parZip(Dispatchers.Default, producer1, producer2, transform) + + +/** + * Runs [producer1], [producer2], and [producer3] in parallel on [context], combining their successful results with [transform]. + * If any computation fails with an [Err], the others are cancelled, and the error is returned as [Err]. + * + * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with the [context] argument. + * If the combined context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * **WARNING** If the combined context has a single-threaded [ContinuationInterceptor], + * this function will not run [producer1], [producer2], and [producer3] in parallel. + */ +public suspend inline fun parZip( + context: CoroutineContext = EmptyCoroutineContext, + crossinline producer1: Producer, + crossinline producer2: Producer, + crossinline producer3: Producer, + crossinline transform: suspend CoroutineScope.(T1, T2, T3) -> V, +): Result { + contract { + callsInPlace(producer1, InvocationKind.AT_MOST_ONCE) + callsInPlace(producer2, InvocationKind.AT_MOST_ONCE) + callsInPlace(producer3, InvocationKind.AT_MOST_ONCE) + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + } + return try { + coroutineScope { + val d1 = async(context) { producer1().valueOrThrowParZipException } + val d2 = async(context) { producer2().valueOrThrowParZipException } + val d3 = async(context) { producer3().valueOrThrowParZipException } + val values = awaitAll(d1, d2, d3) + Ok( + @Suppress("UNCHECKED_CAST") + transform( + values[0] as T1, + values[1] as T2, + values[2] as T3 + ) + ) + } + } catch (e: ParZipException) { + e.toErr() + } +} + +/** + * Runs [producer1], [producer2], [producer3], and [producer4] in parallel on [context], combining their successful results with [transform]. + * If any computation fails with an [Err], the others are cancelled, and the error is returned as [Err]. + * + * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with the [context] argument. + * If the combined context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * **WARNING** If the combined context has a single-threaded [ContinuationInterceptor], + * this function will not run [producer1], [producer2], [producer3], and [producer4] in parallel. + */ +public suspend inline fun parZip( + context: CoroutineContext = EmptyCoroutineContext, + crossinline producer1: Producer, + crossinline producer2: Producer, + crossinline producer3: Producer, + crossinline producer4: Producer, + crossinline transform: suspend CoroutineScope.(T1, T2, T3, T4) -> V, +): Result { + contract { + callsInPlace(producer1, InvocationKind.AT_MOST_ONCE) + callsInPlace(producer2, InvocationKind.AT_MOST_ONCE) + callsInPlace(producer3, InvocationKind.AT_MOST_ONCE) + callsInPlace(producer4, InvocationKind.AT_MOST_ONCE) + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + } + return try { + coroutineScope { + val d1 = async(context) { producer1().valueOrThrowParZipException } + val d2 = async(context) { producer2().valueOrThrowParZipException } + val d3 = async(context) { producer3().valueOrThrowParZipException } + val d4 = async(context) { producer4().valueOrThrowParZipException } + val values = awaitAll(d1, d2, d3, d4) + Ok( + @Suppress("UNCHECKED_CAST") + transform( + values[0] as T1, + values[1] as T2, + values[2] as T3, + values[3] as T4 + ) + ) + } + } catch (e: ParZipException) { + e.toErr() + } +} + +/** + * Runs [producer1], [producer2], [producer3], [producer4], and [producer5] in parallel on [context], combining their successful results with [transform]. + * If any computation fails with an [Err], the others are cancelled, and the error is returned as [Err]. + * + * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with the [context] argument. + * If the combined context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * **WARNING** If the combined context has a single-threaded [ContinuationInterceptor], + * this function will not run [producer1], [producer2], [producer3], [producer4], and [producer5] in parallel. + */ +public suspend inline fun parZip( + context: CoroutineContext = EmptyCoroutineContext, + crossinline producer1: Producer, + crossinline producer2: Producer, + crossinline producer3: Producer, + crossinline producer4: Producer, + crossinline producer5: Producer, + crossinline transform: suspend CoroutineScope.(T1, T2, T3, T4, T5) -> V, +): Result { + contract { + callsInPlace(producer1, InvocationKind.AT_MOST_ONCE) + callsInPlace(producer2, InvocationKind.AT_MOST_ONCE) + callsInPlace(producer3, InvocationKind.AT_MOST_ONCE) + callsInPlace(producer4, InvocationKind.AT_MOST_ONCE) + callsInPlace(producer5, InvocationKind.AT_MOST_ONCE) + callsInPlace(transform, InvocationKind.AT_MOST_ONCE) + } + return try { + coroutineScope { + val d1 = async(context) { producer1().valueOrThrowParZipException } + val d2 = async(context) { producer2().valueOrThrowParZipException } + val d3 = async(context) { producer3().valueOrThrowParZipException } + val d4 = async(context) { producer4().valueOrThrowParZipException } + val d5 = async(context) { producer5().valueOrThrowParZipException } + val values = awaitAll(d1, d2, d3, d4, d5) + Ok( + @Suppress("UNCHECKED_CAST") + transform( + values[0] as T1, + values[1] as T2, + values[2] as T3, + values[3] as T4, + values[4] as T5 + ) + ) + } + } catch (e: ParZipException) { + e.toErr() + } +} diff --git a/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/ParZipTest.kt b/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/ParZipTest.kt new file mode 100644 index 0000000..c9fc75a --- /dev/null +++ b/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/ParZipTest.kt @@ -0,0 +1,45 @@ +package com.github.michaelbull.result.coroutines + +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.and +import com.github.michaelbull.result.zip +import com.github.michaelbull.result.zipOrAccumulate +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ParZipTest { + + data class ZipData3(val a: String, val b: Int, val c: Boolean) + data class ZipData4(val a: String, val b: Int, val c: Boolean, val d: Double) + data class ZipData5(val a: String, val b: Int, val c: Boolean, val d: Double, val e: Char) + + class Zip { + + @Test + fun returnsTransformedValueIfBothOk() = runTest { + val modifyGate = CompletableDeferred() + + val result = parZip( + { + modifyGate.await() + delay(100) + Ok(10) + }, + { + delay(100) + Ok(20).also { modifyGate.complete(0) } + }, + { v1, v2 -> v1 + v2 } + ) + + assertEquals( + expected = Ok(30), + actual = result, + ) + } + } +} From 4268f0a7f6bb643e5ca6b551d5c46d8250ba5bd4 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 27 Apr 2025 21:38:13 +0700 Subject: [PATCH 2/3] Add comprehensive tests for parZip functionality with various scenarios --- .../michaelbull/result/coroutines/ParZip.kt | 238 +++++------------- .../ParZipTest.kt | 198 +++++++++++++-- 2 files changed, 250 insertions(+), 186 deletions(-) diff --git a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt index b55cf82..c332171 100644 --- a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt +++ b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt @@ -5,215 +5,113 @@ import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result import com.github.michaelbull.result.getOrThrow import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlin.contracts.InvocationKind import kotlin.contracts.contract -import kotlin.coroutines.ContinuationInterceptor -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext import kotlin.jvm.JvmField import kotlin.jvm.Transient private typealias Producer = suspend CoroutineScope.() -> Result -@PublishedApi -internal class ParZipException( +private class ParZipException( @JvmField @Transient val error: Any? ) : RuntimeException("parZip failed with error: $error") -@PublishedApi -internal inline val Result.valueOrThrowParZipException: V - get() = getOrThrow(::ParZipException) - - -@Suppress("UNCHECKED_CAST") -@PublishedApi -internal inline fun ParZipException.toErr(): Result = Err(error as E) - - -/** - * Runs [producer1] and [producer2] in parallel on [context], combining their successful results with [transform]. - * If either computation fails with an [Err], the other is cancelled, and the error is returned as [Err]. - * - * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument. - * If the combined context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. - * **WARNING** If the combined context has a single threaded [ContinuationInterceptor], - * this function will not run [producer1], [producer2] in parallel. - */ -public suspend inline fun parZip( - context: CoroutineContext = EmptyCoroutineContext, - crossinline producer1: Producer, - crossinline producer2: Producer, - crossinline transform: suspend CoroutineScope.(value1: T1, value2: T2) -> V, +private suspend inline fun parZipInternal( + producers: List>, + crossinline transform: suspend CoroutineScope.(values: List) -> V, ): Result { contract { - callsInPlace(producer1, InvocationKind.AT_MOST_ONCE) - callsInPlace(producer2, InvocationKind.AT_MOST_ONCE) callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } return try { coroutineScope { - val d1 = async(context) { producer1().valueOrThrowParZipException } - val d2 = async(context) { producer2().valueOrThrowParZipException } - val values = awaitAll(d1, d2) - Ok( - @Suppress("UNCHECKED_CAST") - transform(values[0] as T1, values[1] as T2) - ) + val values = producers + .map { producer -> async { producer().getOrThrow(::ParZipException) } } + .awaitAll() + Ok(transform(values)) } } catch (e: ParZipException) { - e.toErr() + @Suppress("UNCHECKED_CAST") + Err(e.error as E) } } /** - * Runs [producer1] and [producer2] in parallel on [Dispatchers.Default], combining their successful results with [transform]. + * Runs [producer1] and [producer2] in parallel, combining their successful results with [transform]. * If either computation fails with an [Err], the other is cancelled, and the error is returned as [Err]. */ -public suspend inline fun parZip( - crossinline producer1: Producer, - crossinline producer2: Producer, - crossinline transform: suspend CoroutineScope.(value1: T1, value2: T2) -> V, -): Result = parZip(Dispatchers.Default, producer1, producer2, transform) - +public suspend fun parZip( + producer1: Producer, + producer2: Producer, + transform: suspend CoroutineScope.(T1, T2) -> V, +): Result = + parZipInternal(listOf(producer1, producer2)) { + @Suppress("UNCHECKED_CAST") + transform(it[0] as T1, it[1] as T2) + } /** - * Runs [producer1], [producer2], and [producer3] in parallel on [context], combining their successful results with [transform]. + * Runs [producer1], [producer2], and [producer3] in parallel, combining their successful results with [transform]. * If any computation fails with an [Err], the others are cancelled, and the error is returned as [Err]. - * - * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with the [context] argument. - * If the combined context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. - * **WARNING** If the combined context has a single-threaded [ContinuationInterceptor], - * this function will not run [producer1], [producer2], and [producer3] in parallel. */ -public suspend inline fun parZip( - context: CoroutineContext = EmptyCoroutineContext, - crossinline producer1: Producer, - crossinline producer2: Producer, - crossinline producer3: Producer, - crossinline transform: suspend CoroutineScope.(T1, T2, T3) -> V, -): Result { - contract { - callsInPlace(producer1, InvocationKind.AT_MOST_ONCE) - callsInPlace(producer2, InvocationKind.AT_MOST_ONCE) - callsInPlace(producer3, InvocationKind.AT_MOST_ONCE) - callsInPlace(transform, InvocationKind.AT_MOST_ONCE) +public suspend fun parZip( + producer1: Producer, + producer2: Producer, + producer3: Producer, + transform: suspend CoroutineScope.(T1, T2, T3) -> V, +): Result = + parZipInternal(listOf(producer1, producer2, producer3)) { + @Suppress("UNCHECKED_CAST") + transform( + it[0] as T1, + it[1] as T2, + it[2] as T3 + ) } - return try { - coroutineScope { - val d1 = async(context) { producer1().valueOrThrowParZipException } - val d2 = async(context) { producer2().valueOrThrowParZipException } - val d3 = async(context) { producer3().valueOrThrowParZipException } - val values = awaitAll(d1, d2, d3) - Ok( - @Suppress("UNCHECKED_CAST") - transform( - values[0] as T1, - values[1] as T2, - values[2] as T3 - ) - ) - } - } catch (e: ParZipException) { - e.toErr() - } -} /** - * Runs [producer1], [producer2], [producer3], and [producer4] in parallel on [context], combining their successful results with [transform]. + * Runs [producer1], [producer2], [producer3], and [producer4] in parallel, combining their successful results with [transform]. * If any computation fails with an [Err], the others are cancelled, and the error is returned as [Err]. - * - * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with the [context] argument. - * If the combined context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. - * **WARNING** If the combined context has a single-threaded [ContinuationInterceptor], - * this function will not run [producer1], [producer2], [producer3], and [producer4] in parallel. */ -public suspend inline fun parZip( - context: CoroutineContext = EmptyCoroutineContext, - crossinline producer1: Producer, - crossinline producer2: Producer, - crossinline producer3: Producer, - crossinline producer4: Producer, - crossinline transform: suspend CoroutineScope.(T1, T2, T3, T4) -> V, -): Result { - contract { - callsInPlace(producer1, InvocationKind.AT_MOST_ONCE) - callsInPlace(producer2, InvocationKind.AT_MOST_ONCE) - callsInPlace(producer3, InvocationKind.AT_MOST_ONCE) - callsInPlace(producer4, InvocationKind.AT_MOST_ONCE) - callsInPlace(transform, InvocationKind.AT_MOST_ONCE) +public suspend fun parZip( + producer1: Producer, + producer2: Producer, + producer3: Producer, + producer4: Producer, + transform: suspend CoroutineScope.(T1, T2, T3, T4) -> V, +): Result = + parZipInternal(listOf(producer1, producer2, producer3, producer4)) { + @Suppress("UNCHECKED_CAST") + transform( + it[0] as T1, + it[1] as T2, + it[2] as T3, + it[3] as T4 + ) } - return try { - coroutineScope { - val d1 = async(context) { producer1().valueOrThrowParZipException } - val d2 = async(context) { producer2().valueOrThrowParZipException } - val d3 = async(context) { producer3().valueOrThrowParZipException } - val d4 = async(context) { producer4().valueOrThrowParZipException } - val values = awaitAll(d1, d2, d3, d4) - Ok( - @Suppress("UNCHECKED_CAST") - transform( - values[0] as T1, - values[1] as T2, - values[2] as T3, - values[3] as T4 - ) - ) - } - } catch (e: ParZipException) { - e.toErr() - } -} /** - * Runs [producer1], [producer2], [producer3], [producer4], and [producer5] in parallel on [context], combining their successful results with [transform]. + * Runs [producer1], [producer2], [producer3], [producer4], and [producer5] in parallel, combining their successful results with [transform]. * If any computation fails with an [Err], the others are cancelled, and the error is returned as [Err]. - * - * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with the [context] argument. - * If the combined context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. - * **WARNING** If the combined context has a single-threaded [ContinuationInterceptor], - * this function will not run [producer1], [producer2], [producer3], [producer4], and [producer5] in parallel. */ -public suspend inline fun parZip( - context: CoroutineContext = EmptyCoroutineContext, - crossinline producer1: Producer, - crossinline producer2: Producer, - crossinline producer3: Producer, - crossinline producer4: Producer, - crossinline producer5: Producer, - crossinline transform: suspend CoroutineScope.(T1, T2, T3, T4, T5) -> V, -): Result { - contract { - callsInPlace(producer1, InvocationKind.AT_MOST_ONCE) - callsInPlace(producer2, InvocationKind.AT_MOST_ONCE) - callsInPlace(producer3, InvocationKind.AT_MOST_ONCE) - callsInPlace(producer4, InvocationKind.AT_MOST_ONCE) - callsInPlace(producer5, InvocationKind.AT_MOST_ONCE) - callsInPlace(transform, InvocationKind.AT_MOST_ONCE) +public suspend fun parZip( + producer1: Producer, + producer2: Producer, + producer3: Producer, + producer4: Producer, + producer5: Producer, + transform: suspend CoroutineScope.(T1, T2, T3, T4, T5) -> V, +): Result = + parZipInternal(listOf(producer1, producer2, producer3, producer4, producer5)) { + @Suppress("UNCHECKED_CAST") + transform( + it[0] as T1, + it[1] as T2, + it[2] as T3, + it[3] as T4, + it[4] as T5 + ) } - return try { - coroutineScope { - val d1 = async(context) { producer1().valueOrThrowParZipException } - val d2 = async(context) { producer2().valueOrThrowParZipException } - val d3 = async(context) { producer3().valueOrThrowParZipException } - val d4 = async(context) { producer4().valueOrThrowParZipException } - val d5 = async(context) { producer5().valueOrThrowParZipException } - val values = awaitAll(d1, d2, d3, d4, d5) - Ok( - @Suppress("UNCHECKED_CAST") - transform( - values[0] as T1, - values[1] as T2, - values[2] as T3, - values[3] as T4, - values[4] as T5 - ) - ) - } - } catch (e: ParZipException) { - e.toErr() - } -} diff --git a/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/ParZipTest.kt b/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/ParZipTest.kt index c9fc75a..7d8d1e7 100644 --- a/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/ParZipTest.kt +++ b/kotlin-result-coroutines/src/commonTest/kotlin/com.github.michaelbull.result.coroutines/ParZipTest.kt @@ -2,44 +2,210 @@ package com.github.michaelbull.result.coroutines import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok -import com.github.michaelbull.result.and -import com.github.michaelbull.result.zip -import com.github.michaelbull.result.zipOrAccumulate +import com.github.michaelbull.result.Result import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import kotlin.test.Test import kotlin.test.assertEquals +private suspend inline fun simulateDelay() = delay(100) + +private suspend fun produceErr(error: String): Result { + simulateDelay() + return Err(error) +} + +private suspend fun produceOk(value: V): Result { + simulateDelay() + return Ok(value) +} + class ParZipTest { data class ZipData3(val a: String, val b: Int, val c: Boolean) data class ZipData4(val a: String, val b: Int, val c: Boolean, val d: Double) data class ZipData5(val a: String, val b: Int, val c: Boolean, val d: Double, val e: Char) - class Zip { + @Test + fun parZip2ReturnsTransformedValueIfBothOk() = runTest { + val modifyGate = CompletableDeferred() + + val result = withContext(Dispatchers.Default) { + parZip( + { + modifyGate.await() + produceOk(value = "producer1") + }, + { + modifyGate.complete(Unit) + produceOk(value = "producer2") + }, + { v1, v2 -> + simulateDelay() + v1 to v2 + } + ) + } + + assertEquals( + expected = Ok("producer1" to "producer2"), + actual = result, + ) + } - @Test - fun returnsTransformedValueIfBothOk() = runTest { - val modifyGate = CompletableDeferred() + @Test + fun parZip2ReturnsErrIfOneOfTwoErr() = runTest { + val modifyGate = CompletableDeferred() - val result = parZip( + val result = withContext(Dispatchers.Default) { + parZip( { modifyGate.await() - delay(100) - Ok(10) + produceOk(value = "producer1") }, { - delay(100) - Ok(20).also { modifyGate.complete(0) } + modifyGate.complete(Unit) + produceErr(error = "failed") }, - { v1, v2 -> v1 + v2 } + { v1, v2 -> + simulateDelay() + v1 to v2 + } + ) + } + + assertEquals( + expected = Err("failed"), + actual = result, + ) + } + + @Test + fun parZip3ReturnsTransformedValueIfAllOk() = runTest { + val result = withContext(Dispatchers.Default) { + parZip( + { produceOk(value = "producer1") }, + { produceOk(value = 42) }, + { produceOk(value = true) }, + { v1, v2, v3 -> + simulateDelay() + ZipData3(v1, v2, v3) + } + ) + } + + assertEquals( + expected = Ok(ZipData3("producer1", 42, true)), + actual = result, + ) + } + + @Test + fun parZip3ReturnsErrIfOneOfThreeErr() = runTest { + val result = withContext(Dispatchers.Default) { + parZip( + { produceOk(value = "producer1") }, + { produceErr(error = "failed") }, + { produceOk(value = true) }, + { v1, v2, v3 -> + simulateDelay() + ZipData3(v1, v2, v3) + } + ) + } + + assertEquals( + expected = Err("failed"), + actual = result, + ) + } + + @Test + fun parZip4ReturnsTransformedValueIfAllOk() = runTest { + val result = withContext(Dispatchers.Default) { + parZip( + { produceOk(value = "producer1") }, + { produceOk(value = 42) }, + { produceOk(value = true) }, + { produceOk(value = 3.14) }, + { v1, v2, v3, v4 -> + simulateDelay() + ZipData4(v1, v2, v3, v4) + } + ) + } + + assertEquals( + expected = Ok(ZipData4("producer1", 42, true, 3.14)), + actual = result, + ) + } + + @Test + fun parZip4ReturnsErrIfOneOfFourErr() = runTest { + val result = withContext(Dispatchers.Default) { + parZip( + { produceOk(value = "producer1") }, + { produceErr(error = "failed") }, + { produceOk(value = true) }, + { produceOk(value = 3.14) }, + { v1, v2, v3, v4 -> + simulateDelay() + ZipData4(v1, v2, v3, v4) + } ) + } - assertEquals( - expected = Ok(30), - actual = result, + assertEquals( + expected = Err("failed"), + actual = result, + ) + } + + @Test + fun parZip5ReturnsTransformedValueIfAllOk() = runTest { + val result = withContext(Dispatchers.Default) { + parZip( + { produceOk(value = "producer1") }, + { produceOk(value = 42) }, + { produceOk(value = true) }, + { produceOk(value = 3.14) }, + { produceOk(value = 'X') }, + { v1, v2, v3, v4, v5 -> + simulateDelay() + ZipData5(v1, v2, v3, v4, v5) + } ) } + + assertEquals( + expected = Ok(ZipData5("producer1", 42, true, 3.14, 'X')), + actual = result, + ) + } + + @Test + fun parZip5ReturnsErrIfOneOfFiveErr() = runTest { + val result = withContext(Dispatchers.Default) { + parZip( + { produceOk(value = "producer1") }, + { produceErr(error = "failed") }, + { produceOk(value = true) }, + { produceOk(value = 3.14) }, + { produceOk(value = 'X') }, + { v1, v2, v3, v4, v5 -> + simulateDelay() + ZipData5(v1, v2, v3, v4, v5) + } + ) + } + + assertEquals( + expected = Err("failed"), + actual = result, + ) } } From 1e06009015e832a166f0b69ab56e154029840f34 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Mon, 28 Apr 2025 22:17:55 +0700 Subject: [PATCH 3/3] Refactor parZipInternal to use coroutineBinding --- .../michaelbull/result/coroutines/ParZip.kt | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt index c332171..8295a64 100644 --- a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt +++ b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/ParZip.kt @@ -1,24 +1,15 @@ 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 com.github.michaelbull.result.getOrThrow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlin.contracts.InvocationKind import kotlin.contracts.contract -import kotlin.jvm.JvmField -import kotlin.jvm.Transient private typealias Producer = suspend CoroutineScope.() -> Result -private class ParZipException( - @JvmField @Transient val error: Any? -) : RuntimeException("parZip failed with error: $error") - private suspend inline fun parZipInternal( producers: List>, crossinline transform: suspend CoroutineScope.(values: List) -> V, @@ -26,16 +17,11 @@ private suspend inline fun parZipInternal( contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } - return try { - coroutineScope { - val values = producers - .map { producer -> async { producer().getOrThrow(::ParZipException) } } - .awaitAll() - Ok(transform(values)) - } - } catch (e: ParZipException) { - @Suppress("UNCHECKED_CAST") - Err(e.error as E) + return coroutineBinding { + val values = producers + .map { producer -> async { producer().bind() } } + .awaitAll() + transform(values) } }