diff --git a/arrow-libs/core/arrow-core-test/api/arrow-core-test.api b/arrow-libs/core/arrow-core-test/api/arrow-core-test.api index 8bf623467eb..d6324cf5073 100644 --- a/arrow-libs/core/arrow-core-test/api/arrow-core-test.api +++ b/arrow-libs/core/arrow-core-test/api/arrow-core-test.api @@ -61,6 +61,7 @@ public final class arrow/core/test/generators/GeneratorsKt { public static final fun nonZeroInt (Lio/kotest/property/Arb$Companion;)Lio/kotest/property/Arb; public static final fun option (Lio/kotest/property/Arb$Companion;Lio/kotest/property/Arb;)Lio/kotest/property/Arb; public static final fun or (Lio/kotest/property/Arb;Lio/kotest/property/Arb;)Lio/kotest/property/Arb; + public static final fun result (Lio/kotest/property/Arb$Companion;Lio/kotest/property/Arb;)Lio/kotest/property/Arb; public static final fun sequence (Lio/kotest/property/Arb$Companion;Lio/kotest/property/Arb;)Lio/kotest/property/Arb; public static final fun shortSmall (Lio/kotest/property/Arb$Companion;)Lio/kotest/property/Arb; public static final fun suspendFunThatReturnsAnyLeft (Lio/kotest/property/Arb$Companion;)Lio/kotest/property/Arb; @@ -79,6 +80,11 @@ public final class arrow/core/test/generators/GeneratorsKt { public static final fun validated (Lio/kotest/property/Arb$Companion;Lio/kotest/property/Arb;Lio/kotest/property/Arb;)Lio/kotest/property/Arb; } +public final class arrow/core/test/generators/UtilsKt { + public static final fun suspend (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun suspend (Ljava/lang/Throwable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class arrow/core/test/laws/FxLaws { public static final field INSTANCE Larrow/core/test/laws/FxLaws; public final fun eager (Lio/kotest/property/Arb;Lio/kotest/property/Arb;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;)Ljava/util/List; diff --git a/arrow-libs/core/arrow-core-test/src/commonMain/kotlin/arrow/core/test/generators/Generators.kt b/arrow-libs/core/arrow-core-test/src/commonMain/kotlin/arrow/core/test/generators/Generators.kt index 82a4f6c40b1..8daf5a919b0 100644 --- a/arrow-libs/core/arrow-core-test/src/commonMain/kotlin/arrow/core/test/generators/Generators.kt +++ b/arrow-libs/core/arrow-core-test/src/commonMain/kotlin/arrow/core/test/generators/Generators.kt @@ -40,6 +40,8 @@ import io.kotest.property.arbitrary.of import io.kotest.property.arbitrary.orNull import io.kotest.property.arbitrary.short import io.kotest.property.arbitrary.string +import kotlin.Result.Companion.failure +import kotlin.Result.Companion.success import kotlin.jvm.JvmOverloads import kotlin.math.abs import kotlin.random.nextInt @@ -62,6 +64,9 @@ public fun Arb.Companion.functionToA(arb: Arb): Arb<() -> A> = public fun Arb.Companion.throwable(): Arb = Arb.of(listOf(RuntimeException(), NoSuchElementException(), IllegalArgumentException())) +public fun Arb.Companion.result(arbA: Arb): Arb> = + Arb.choice(arbA.map(::success), throwable().map(::failure)) + public fun Arb.Companion.doubleSmall(): Arb = Arb.numericDoubles(from = 0.0, to = 100.0) diff --git a/arrow-libs/core/arrow-core-test/src/commonMain/kotlin/arrow/core/test/generators/utils.kt b/arrow-libs/core/arrow-core-test/src/commonMain/kotlin/arrow/core/test/generators/utils.kt new file mode 100644 index 00000000000..b6d8f37299e --- /dev/null +++ b/arrow-libs/core/arrow-core-test/src/commonMain/kotlin/arrow/core/test/generators/utils.kt @@ -0,0 +1,30 @@ +package arrow.core.test.generators + +import kotlin.coroutines.Continuation +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.intercepted +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.startCoroutine +import kotlinx.coroutines.Dispatchers + +public suspend fun Throwable.suspend(): Nothing = + suspendCoroutineUninterceptedOrReturn { cont -> + suspend { throw this }.startCoroutine( + Continuation(Dispatchers.Default) { + cont.intercepted().resumeWith(it) + } + ) + + COROUTINE_SUSPENDED + } + +public suspend fun A.suspend(): A = + suspendCoroutineUninterceptedOrReturn { cont -> + suspend { this }.startCoroutine( + Continuation(Dispatchers.Default) { + cont.intercepted().resumeWith(it) + } + ) + + COROUTINE_SUSPENDED + } diff --git a/arrow-libs/core/arrow-core/api/arrow-core.api b/arrow-libs/core/arrow-core/api/arrow-core.api index b2494371901..6b2b567d1ce 100644 --- a/arrow-libs/core/arrow-core/api/arrow-core.api +++ b/arrow-libs/core/arrow-core/api/arrow-core.api @@ -595,6 +595,7 @@ public final class arrow/core/IterableKt { public static final fun separateValidated (Ljava/lang/Iterable;)Lkotlin/Pair; public static final fun sequenceEither (Ljava/lang/Iterable;)Larrow/core/Either; public static final fun sequenceOption (Ljava/lang/Iterable;)Larrow/core/Option; + public static final fun sequenceResult (Ljava/lang/Iterable;)Ljava/lang/Object; public static final fun sequenceValidated (Ljava/lang/Iterable;)Larrow/core/Validated; public static final fun sequenceValidated (Ljava/lang/Iterable;Larrow/typeclasses/Semigroup;)Larrow/core/Validated; public static final fun singleOrNone (Ljava/lang/Iterable;)Larrow/core/Option; @@ -603,6 +604,7 @@ public final class arrow/core/IterableKt { public static final fun tail (Ljava/lang/Iterable;)Ljava/util/List; public static final fun traverseEither (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Larrow/core/Either; public static final fun traverseOption (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Larrow/core/Option; + public static final fun traverseResult (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public static final fun traverseValidated (Ljava/lang/Iterable;Larrow/typeclasses/Semigroup;Lkotlin/jvm/functions/Function1;)Larrow/core/Validated; public static final fun traverseValidated (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Larrow/core/Validated; public static final fun unalign (Ljava/lang/Iterable;)Lkotlin/Pair; @@ -1383,6 +1385,22 @@ public final class arrow/core/PredefKt { public static final fun identity (Ljava/lang/Object;)Ljava/lang/Object; } +public final class arrow/core/ResultKt { + public static final fun composeErrors ([Ljava/lang/Throwable;)Ljava/lang/Throwable; + public static final fun flatMap (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun handleErrorWith (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun redeemWith (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun zip (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function10;)Ljava/lang/Object; + public static final fun zip (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function9;)Ljava/lang/Object; + public static final fun zip (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function8;)Ljava/lang/Object; + public static final fun zip (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function7;)Ljava/lang/Object; + public static final fun zip (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function6;)Ljava/lang/Object; + public static final fun zip (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function5;)Ljava/lang/Object; + public static final fun zip (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function4;)Ljava/lang/Object; + public static final fun zip (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Ljava/lang/Object; + public static final fun zip (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; +} + public final class arrow/core/SequenceKt { public static final fun align (Lkotlin/sequences/Sequence;Lkotlin/sequences/Sequence;)Lkotlin/sequences/Sequence; public static final fun align (Lkotlin/sequences/Sequence;Lkotlin/sequences/Sequence;Lkotlin/jvm/functions/Function1;)Lkotlin/sequences/Sequence; @@ -2386,12 +2404,14 @@ public final class arrow/core/ValidatedKt { public abstract interface class arrow/core/computations/EitherEffect : arrow/continuations/Effect { public abstract fun bind (Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun bind (Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bind (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun ensure (ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class arrow/core/computations/EitherEffect$DefaultImpls { public static fun bind (Larrow/core/computations/EitherEffect;Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun bind (Larrow/core/computations/EitherEffect;Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/computations/EitherEffect;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun ensure (Larrow/core/computations/EitherEffect;ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -2445,6 +2465,7 @@ public abstract interface class arrow/core/computations/RestrictedEitherEffect : public final class arrow/core/computations/RestrictedEitherEffect$DefaultImpls { public static fun bind (Larrow/core/computations/RestrictedEitherEffect;Larrow/core/Either;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun bind (Larrow/core/computations/RestrictedEitherEffect;Larrow/core/Validated;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bind (Larrow/core/computations/RestrictedEitherEffect;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun ensure (Larrow/core/computations/RestrictedEitherEffect;ZLkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -2473,6 +2494,13 @@ public final class arrow/core/computations/RestrictedOptionEffect$DefaultImpls { public static fun ensure (Larrow/core/computations/RestrictedOptionEffect;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class arrow/core/computations/ResultEffect { + public static final field INSTANCE Larrow/core/computations/ResultEffect; + public final fun bind (Larrow/core/Either;)Ljava/lang/Object; + public final fun bind (Larrow/core/Validated;)Ljava/lang/Object; + public final fun bind (Ljava/lang/Object;)Ljava/lang/Object; +} + public final class arrow/core/computations/either { public static final field INSTANCE Larrow/core/computations/either; public final fun eager (Lkotlin/jvm/functions/Function2;)Larrow/core/Either; @@ -2497,6 +2525,11 @@ public final class arrow/core/computations/option { public final fun invoke (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class arrow/core/computations/result { + public static final field INSTANCE Larrow/core/computations/result; + public final fun invoke-IoAF18A (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + public abstract interface class arrow/typeclasses/Monoid : arrow/typeclasses/Semigroup { public static final field Companion Larrow/typeclasses/Monoid$Companion; public static fun Boolean ()Larrow/typeclasses/Monoid; diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Iterable.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Iterable.kt index c0629ce5cbf..f5987388dc4 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Iterable.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Iterable.kt @@ -6,6 +6,7 @@ import arrow.core.Either.Left import arrow.core.Either.Right import arrow.typeclasses.Monoid import arrow.typeclasses.Semigroup +import kotlin.Result.Companion.success import kotlin.collections.foldRight as _foldRight public inline fun Iterable.zip( @@ -292,25 +293,32 @@ public inline fun Iterable.foldRight(initial: B, operation: (A, acc: B else -> reversed().fold(initial) { acc, a -> operation(a, acc) } } -public inline fun Iterable.traverseEither(f: (A) -> Either): Either> { - val acc = mutableListOf() - forEach { a -> +public inline fun Iterable.traverseEither(f: (A) -> Either): Either> = + map { a -> when (val res = f(a)) { - is Right -> acc.add(res.value) + is Right -> res.value is Left -> return@traverseEither res } - } - return acc.right() -} + }.right() public fun Iterable>.sequenceEither(): Either> = traverseEither(::identity) +public inline fun Iterable.traverseResult(f: (A) -> Result): Result> = + map { a -> + f(a).fold(::identity) { throwable -> + return@traverseResult Result.failure(throwable) + } + }.let(::success) + +public fun Iterable>.sequenceResult(): Result> = + traverseResult(::identity) + public inline fun Iterable.traverseValidated( semigroup: Semigroup, f: (A) -> Validated ): Validated> = semigroup.run { - fold(Valid(mutableListOf()) as Validated>) { acc, a -> + fold(Valid(ArrayList(collectionSizeOrDefault(10))) as Validated>) { acc, a -> when (val res = f(a)) { is Validated.Valid -> when (acc) { is Valid -> acc.also { it.value.add(res.value) } @@ -333,22 +341,19 @@ public fun Iterable>.sequenceValidated(semigroup: Semigro public fun Iterable>.sequenceValidated(): ValidatedNel> = traverseValidated(Semigroup.nonEmptyList(), ::identity) -public inline fun Iterable.traverseOption(f: (A) -> Option): Option> { - val acc = mutableListOf() - forEach { a -> +public inline fun Iterable.traverseOption(f: (A) -> Option): Option> = + map { a -> when (val res = f(a)) { - is Some -> acc.add(res.value) + is Some -> res.value is None -> return@traverseOption res } - } - return acc.some() -} + }.some() public fun Iterable>.sequenceOption(): Option> = this.traverseOption { it } public fun Iterable.void(): List = - map { Unit } + map { } @Deprecated(FoldRightDeprecation) public fun List.foldRight(lb: Eval, f: (A, Eval) -> Eval): Eval { @@ -791,17 +796,13 @@ public fun Iterable.lastOrNone(): Option = * Returns the last element as [Some(element)][Some] matching the given [predicate], or [None] if no such element was found. */ public inline fun Iterable.lastOrNone(predicate: (T) -> Boolean): Option { - val list = mutableListOf() + var value: Any? = EmptyValue for (element in this) { if (predicate(element)) { - if (list.isEmpty()) { - list.add(element) - } else { - list[0] = element - } + value = element } } - return list.firstOrNone() + return if (value === EmptyValue) None else Some(EmptyValue.unbox(value)) } /** diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Result.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Result.kt new file mode 100644 index 00000000000..6947a89675f --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Result.kt @@ -0,0 +1,199 @@ +package arrow.core + +import kotlin.Result.Companion.failure +import kotlin.Result.Companion.success + +@PublishedApi +internal inline val UnitResult: Result + inline get() = success(Unit) + +/** + * Compose a [transform] operation on the success value [A] into [B] whilst flattening [Result]. + * @see mapCatching if you want run a function that catches and maps with `(A) -> B` + */ +public inline fun Result.flatMap(transform: (value: A) -> Result): Result = + map(transform).fold(::identity, ::failure) + +/** + * Compose a recovering [transform] operation on the failure value [Throwable] whilst flattening [Result]. + * @see recoverCatching if you want run a function that catches and maps recovers with `(Throwable) -> A`. + */ +public inline fun Result.handleErrorWith(transform: (throwable: Throwable) -> Result): Result = + when (val exception = exceptionOrNull()) { + null -> this + else -> transform(exception) + } + +/** + * Compose both: + * - a [transform] operation on the success value [A] into [B] whilst flattening [Result]. + * - a recovering [transform] operation on the failure value [Throwable] whilst flattening [Result]. + * + * Combining the powers of [flatMap] and [handleErrorWith]. + */ +public inline fun Result.redeemWith( + handleErrorWith: (throwable: Throwable) -> Result, + transform: (value: A) -> Result +): Result = fold(transform, handleErrorWith) + +/** + * Combines n-arity independent [Result] values with a [transform] function. + */ +public inline fun Result.zip(b: Result, transform: (A, B) -> C): Result = + zip( + b, + UnitResult, + UnitResult, + UnitResult, + UnitResult, + UnitResult, + UnitResult, + UnitResult, + UnitResult + ) { a, b, _, _, _, _, _, _, _, _ -> transform(a, b) } + +public inline fun Result.zip(b: Result, c: Result, transform: (A, B, C) -> D): Result = + zip( + b, + c, + UnitResult, + UnitResult, + UnitResult, + UnitResult, + UnitResult, + UnitResult, + UnitResult + ) { a, b, c, _, _, _, _, _, _, _ -> transform(a, b, c) } + +public inline fun Result.zip( + b: Result, + c: Result, + d: Result, + transform: (A, B, C, D) -> E +): Result = + zip( + b, + c, + d, + UnitResult, + UnitResult, + UnitResult, + UnitResult, + UnitResult, + UnitResult + ) { a, b, c, d, _, _, _, _, _, _ -> transform(a, b, c, d) } + +public inline fun Result.zip( + b: Result, + c: Result, + d: Result, + e: Result, + transform: (A, B, C, D, E) -> F +): Result = + zip(b, c, d, e, UnitResult, UnitResult, UnitResult, UnitResult, UnitResult) { a, b, c, d, e, f, _, _, _, _ -> + transform( + a, + b, + c, + d, + e + ) + } + +public inline fun Result.zip( + b: Result, + c: Result, + d: Result, + e: Result, + f: Result, + transform: (A, B, C, D, E, F) -> G +): Result = + zip(b, c, d, e, f, UnitResult, UnitResult, UnitResult, UnitResult) { a, b, c, d, e, f, _, _, _, _ -> + transform( + a, + b, + c, + d, + e, + f + ) + } + +public inline fun Result.zip( + b: Result, + c: Result, + d: Result, + e: Result, + f: Result, + g: Result, + transform: (A, B, C, D, E, F, G) -> H +): Result = + zip(b, c, d, e, f, g, UnitResult, UnitResult, UnitResult) { a, b, c, d, e, f, g, _, _, _ -> transform(a, b, c, d, e, f, g) } + +public inline fun Result.zip( + b: Result, + c: Result, + d: Result, + e: Result, + f: Result, + g: Result, + h: Result, + transform: (A, B, C, D, E, F, G, H) -> I +): Result = + zip(b, c, d, e, f, g, h, UnitResult, UnitResult) { a, b, c, d, e, f, g, h, _, _ -> transform(a, b, c, d, e, f, g, h) } + +public inline fun Result.zip( + b: Result, + c: Result, + d: Result, + e: Result, + f: Result, + g: Result, + h: Result, + i: Result, + transform: (A, B, C, D, E, F, G, H, I) -> J +): Result = + zip(b, c, d, e, f, g, h, i, UnitResult) { a, b, c, d, e, f, g, h, i, _ -> transform(a, b, c, d, e, f, g, h, i) } + +public inline fun Result.zip( + b: Result, + c: Result, + d: Result, + e: Result, + f: Result, + g: Result, + h: Result, + i: Result, + j: Result, + transform: (A, B, C, D, E, F, G, H, I, J) -> K +): Result = Nullable.zip( + getOrNull(), + b.getOrNull(), + c.getOrNull(), + d.getOrNull(), + e.getOrNull(), + f.getOrNull(), + g.getOrNull(), + h.getOrNull(), + i.getOrNull(), + j.getOrNull(), + transform +)?.let { success(it) } ?: composeErrors( + exceptionOrNull(), + b.exceptionOrNull(), + c.exceptionOrNull(), + d.exceptionOrNull(), + e.exceptionOrNull(), + f.exceptionOrNull(), + g.exceptionOrNull(), + h.exceptionOrNull(), + i.exceptionOrNull(), + j.exceptionOrNull(), +)!!.let(::failure) + +@PublishedApi +internal fun composeErrors(vararg other: Throwable?): Throwable? = + other.reduceOrNull { a, b -> + Nullable.zip(a, b, Throwable::addSuppressed) + a ?: b + } diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/either.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/either.kt index a7572a5040e..a84b77ebffb 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/either.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/either.kt @@ -4,6 +4,7 @@ import arrow.continuations.Effect import arrow.core.Either import arrow.core.Either.Left import arrow.core.Validated +import arrow.core.identity import arrow.core.left import arrow.core.right import kotlin.contracts.ExperimentalContracts @@ -24,6 +25,11 @@ public fun interface EitherEffect : Effect> { is Validated.Invalid -> control().shift(Left(value)) } + public suspend fun Result.bind(transform: (Throwable) -> E): B = + fold(::identity) { throwable -> + control().shift(transform(throwable).left()) + } + /** * Ensure check if the [value] is `true`, * and if it is it allows the `either { }` binding to continue. diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/result.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/result.kt new file mode 100644 index 00000000000..7d72712c6df --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/computations/result.kt @@ -0,0 +1,52 @@ +package arrow.core.computations + +import arrow.core.Either +import arrow.core.Validated +import arrow.core.identity + +/** + * DSL Receiver Syntax for [result]. + */ +public object ResultEffect { + + public fun Result.bind(): A = + getOrThrow() + + public fun Either.bind(): A = + fold({ throw it }, ::identity) + + public fun Validated.bind(): A = + fold({ throw it }, ::identity) +} + +@Suppress("ClassName") +public object result { + + /** + * Provides a computation block for [Result] which is build on top of Kotlin's Result Std operations. + * + * ```kotlin:ank + * import arrow.core.* + * + * fun main() { + * result { // We can safely use assertion based operation inside blocks + * kotlin.require(false) { "Boom" } + * } // Result.Failure(IllegalArgumentException("Boom")) + * + * result { + * Result.failure(RuntimeException("Boom")) + * .recover { 1 } + * .bind() + * } // Result.Success(1) + * + * result { + * val x = Result.success(1).bind() + * val y = Result.success(x + 1).bind() + * x + y + * } // Result.Success(3) + * } + * ``` + */ + public inline operator fun invoke(block: ResultEffect.() -> A): Result = + kotlin.runCatching { block(ResultEffect) } +} diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/IterableTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/IterableTest.kt index 2e3ee8db526..3f56f387670 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/IterableTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/IterableTest.kt @@ -48,6 +48,40 @@ class IterableTest : UnitSpec() { } } + "traverseResult stack-safe" { + // also verifies result order and execution order (l to r) + val acc = mutableListOf() + val res = (0..20_000).traverseResult { a -> + acc.add(a) + Result.success(a) + } + res shouldBe Result.success(acc) + res shouldBe Result.success((0..20_000).toList()) + } + + "traverseResult short-circuit" { + checkAll(Arb.list(Arb.int())) { ints -> + val acc = mutableListOf() + val evens = ints.traverseResult { + if (it % 2 == 0) { + acc.add(it) + Result.success(it) + } else Result.failure(RuntimeException()) + } + acc shouldBe ints.takeWhile { it % 2 == 0 } + evens.fold( + { it shouldBe ints }, + { } + ) + } + } + + "sequenceResult should be consistent with traverseResult" { + checkAll(Arb.list(Arb.int())) { ints -> + ints.map { Result.success(it) }.sequenceResult() shouldBe ints.traverseResult { Result.success(it) } + } + } + "traverseOption is stack-safe" { // also verifies result order and execution order (l to r) val acc = mutableListOf() diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/computations/ResultTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/computations/ResultTest.kt new file mode 100644 index 00000000000..301ffb7a5d9 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/computations/ResultTest.kt @@ -0,0 +1,224 @@ +package arrow.core.computations + +import arrow.core.Eval +import arrow.core.Tuple10 +import arrow.core.composeErrors +import arrow.core.flatMap +import arrow.core.handleErrorWith +import arrow.core.redeemWith +import arrow.core.test.UnitSpec +import arrow.core.test.generators.result +import arrow.core.test.generators.suspend +import arrow.core.test.generators.throwable +import arrow.core.zip +import io.kotest.assertions.fail +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.result.shouldBeFailureOfType +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.string +import io.kotest.property.checkAll +import kotlin.Result.Companion.failure +import kotlin.Result.Companion.success +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.suspendCancellableCoroutine + +class ResultTest : UnitSpec() { + init { + "flatMap" { + checkAll(Arb.result(Arb.int()), Arb.result(Arb.string())) { ints, strs -> + val res = ints.flatMap { strs } + if (ints.isFailure) res shouldBe ints + else res shouldBe strs + } + } + + "handleErrorWith" { + checkAll(Arb.result(Arb.int()), Arb.result(Arb.string())) { ints, strs -> + val res = ints.handleErrorWith { strs } + if (ints.isFailure) res shouldBe strs + else res shouldBe ints + } + } + + "redeemWith" { + checkAll(Arb.result(Arb.int()), Arb.result(Arb.string()), Arb.result(Arb.string())) { ints, failed, success -> + val res = ints.redeemWith({ failed }, { success }) + if (ints.isFailure) res shouldBe failed + else res shouldBe success + } + } + + "zip" { + checkAll( + Arb.result(Arb.int()), + Arb.result(Arb.int()), + Arb.result(Arb.int()), + Arb.result(Arb.int()), + Arb.result(Arb.int()), + Arb.result(Arb.int()), + Arb.result(Arb.int()), + Arb.result(Arb.int()), + Arb.result(Arb.int()), + Arb.result(Arb.int()), + ) { a, b, c, d, e, f, g, h, i, j -> + val res = a.zip(b, c, d, e, f, g, h, i, j, ::Tuple10) + val all = listOf(a, b, c, d, e, f, g, h, i, j) + if (all.all { it.isSuccess }) res shouldBe success( + Tuple10( + a.getOrThrow(), + b.getOrThrow(), + c.getOrThrow(), + d.getOrThrow(), + e.getOrThrow(), + f.getOrThrow(), + g.getOrThrow(), + h.getOrThrow(), + i.getOrThrow(), + j.getOrThrow() + ) + ) else res shouldBe failure( + composeErrors( + a.exceptionOrNull(), + b.exceptionOrNull(), + c.exceptionOrNull(), + d.exceptionOrNull(), + e.exceptionOrNull(), + f.exceptionOrNull(), + g.exceptionOrNull(), + h.exceptionOrNull(), + i.exceptionOrNull(), + j.exceptionOrNull() + ).shouldNotBeNull() + ) + } + } + + "immediate values" { + checkAll(Arb.result(Arb.int())) { res -> + result { + res.bind() + } shouldBe res + } + } + + "suspended value" { + checkAll(Arb.result(Arb.int())) { res -> + result { + res.suspend().bind() + } shouldBe res + } + } + + "Rethrows immediate exceptions" { + checkAll(Arb.throwable(), Arb.int(), Arb.int()) { e, a, b -> + result { + success(a).bind() + success(b).suspend().bind() + throw e + } shouldBe failure(e) + } + } + + "result captures exception" { + checkAll(Arb.throwable(), Arb.int(), Arb.int()) { e, a, b -> + result { + success(a).bind() + success(b).suspend().bind() + e.suspend() + } shouldBe failure(e) + } + } + + "Can short-circuit from nested blocks" { + checkAll(Arb.throwable()) { e -> + result { + val x = eval { + failure(e).suspend().bind() + 5L + } + + x.value() + } shouldBe failure(e) + } + } + + "Can short-circuit suspended from nested blocks" { + checkAll(Arb.throwable().map { failure(it) }) { res -> + result { + val x = eval { + res.suspend().bind() + 5L + } + + x.value() + } shouldBe res + } + } + + "Can short-circuit after bind from nested blocks" { + checkAll(Arb.throwable().map { failure(it) }) { res -> + result { + val x = eval { + Eval.Now(1L).suspend().bind() + res.suspend().bind() + 5L + } + + 1 + } shouldBe res + } + } + + "Short-circuiting cancels KotlinX Coroutines" { + suspend fun completeOnCancellation(latch: CompletableDeferred, cancelled: CompletableDeferred): Unit = + suspendCancellableCoroutine { cont -> + cont.invokeOnCancellation { + if (!cancelled.complete(Unit)) fail("cancelled latch was completed twice") + else Unit + } + + if (!latch.complete(Unit)) fail("latch was completed twice") + else Unit + } + + val scope = CoroutineScope(Dispatchers.Default) + val latch = CompletableDeferred() + val cancelled = CompletableDeferred() + result { + val deferreds: List> = listOf( + scope.async { + completeOnCancellation(latch, cancelled) + success(1).bind() + }, + scope.async { + latch.await() + failure(RuntimeException()).bind() + } + ) + + deferreds.awaitAll().sum() + }.shouldBeFailureOfType() + + cancelled.await() + } + + "Computation blocks run on parent context" { + suspend fun currentContext(): CoroutineContext = + kotlin.coroutines.coroutineContext + + val parentCtx = currentContext() + result { + currentContext() shouldBe parentCtx + } + } + } +} diff --git a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api index 541c91440b0..eda3f8057a9 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api +++ b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api @@ -182,7 +182,6 @@ public final class arrow/fx/coroutines/ParTraverse { public static final fun parSequence (Ljava/lang/Iterable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequence (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parSequence$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun parSequenceEither (Ljava/lang/Iterable;Larrow/typeclasses/Semigroup;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequenceEither (Ljava/lang/Iterable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequenceEither (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parSequenceEither$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; @@ -192,7 +191,6 @@ public final class arrow/fx/coroutines/ParTraverse { public static final fun parSequenceEitherNScoped (Ljava/lang/Iterable;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequenceEitherNScoped (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parSequenceEitherNScoped$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun parSequenceEitherScoped (Ljava/lang/Iterable;Larrow/typeclasses/Semigroup;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequenceEitherScoped (Ljava/lang/Iterable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequenceEitherScoped (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parSequenceEitherScoped$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; @@ -202,9 +200,20 @@ public final class arrow/fx/coroutines/ParTraverse { public static final fun parSequenceNScoped (Ljava/lang/Iterable;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequenceNScoped (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parSequenceNScoped$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun parSequenceResult (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun parSequenceResult$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun parSequenceResultN (Ljava/lang/Iterable;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun parSequenceResultN (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun parSequenceResultN$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun parSequenceResultNScoped (Ljava/lang/Iterable;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun parSequenceResultNScoped (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun parSequenceResultNScoped$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun parSequenceResultScoped (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun parSequenceResultScoped$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun parSequenceScoped (Ljava/lang/Iterable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequenceScoped (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parSequenceScoped$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun parSequenceValidated (Ljava/lang/Iterable;Larrow/typeclasses/Semigroup;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequenceValidated (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Larrow/typeclasses/Semigroup;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parSequenceValidated$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Larrow/typeclasses/Semigroup;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun parSequenceValidatedN (Ljava/lang/Iterable;Larrow/typeclasses/Semigroup;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -213,6 +222,7 @@ public final class arrow/fx/coroutines/ParTraverse { public static final fun parSequenceValidatedNScoped (Ljava/lang/Iterable;Larrow/typeclasses/Semigroup;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequenceValidatedNScoped (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Larrow/typeclasses/Semigroup;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parSequenceValidatedNScoped$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Larrow/typeclasses/Semigroup;ILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun parSequenceValidatedScoped (Ljava/lang/Iterable;Larrow/typeclasses/Semigroup;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parSequenceValidatedScoped (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Larrow/typeclasses/Semigroup;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parSequenceValidatedScoped$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Larrow/typeclasses/Semigroup;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun parTraverse (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -227,6 +237,12 @@ public final class arrow/fx/coroutines/ParTraverse { public static final fun parTraverseN (Ljava/lang/Iterable;ILkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parTraverseN (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parTraverseN$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun parTraverseResult (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun parTraverseResult (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun parTraverseResult$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun parTraverseResultN (Ljava/lang/Iterable;ILkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun parTraverseResultN (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun parTraverseResultN$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun parTraverseValidated (Ljava/lang/Iterable;Larrow/typeclasses/Semigroup;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun parTraverseValidated (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Larrow/typeclasses/Semigroup;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun parTraverseValidated$default (Ljava/lang/Iterable;Lkotlin/coroutines/CoroutineContext;Larrow/typeclasses/Semigroup;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/ParTraverseResult.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/ParTraverseResult.kt new file mode 100644 index 00000000000..f47168d80f7 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/ParTraverseResult.kt @@ -0,0 +1,180 @@ +@file:JvmMultifileClass +@file:JvmName("ParTraverse") + +package arrow.fx.coroutines + +import arrow.core.sequenceResult +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +/** + * Traverses this [Iterable] and runs `suspend CoroutineScope.() -> Result` in [n] parallel operations on [CoroutineContext]. + * If one or more of the tasks returns [Result.failure] then all the [Result.failure] results will be combined using [addSuppressed]. + * + * Cancelling this operation cancels all running tasks. + */ +@JvmName("parSequenceResultNScoped") +public suspend fun Iterable Result>.parSequenceResultN(n: Int): Result> = + parTraverseResultN(Dispatchers.Default, n) { it() } + +public suspend fun Iterable Result>.parSequenceResultN(n: Int): Result> = + parTraverseResultN(Dispatchers.Default, n) { it() } + +/** + * Traverses this [Iterable] and runs `suspend CoroutineScope.() -> Result` in [n] parallel operations on [CoroutineContext]. + * If one or more of the tasks returns [Result.failure] then all the [Result.failure] results will be combined using [addSuppressed]. + * + * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [ctx] 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 in parallel. + * + * Cancelling this operation cancels all running tasks. + */ +@JvmName("parSequenceResultNScoped") +public suspend fun Iterable Result>.parSequenceResultN( + ctx: CoroutineContext = EmptyCoroutineContext, + n: Int +): Result> = + parTraverseResultN(ctx, n) { it() } + +public suspend fun Iterable Result>.parSequenceResultN( + ctx: CoroutineContext = EmptyCoroutineContext, + n: Int +): Result> = + parTraverseResultN(ctx, n) { it() } + +/** + * Traverses this [Iterable] and runs [f] in [n] parallel operations on [Dispatchers.Default]. + * If one or more of the [f] returns [Result.failure] then all the [Result.failure] results will be combined using [addSuppressed]. + * + * Cancelling this operation cancels all running tasks. + */ +public suspend fun Iterable.parTraverseResultN( + n: Int, + f: suspend CoroutineScope.(A) -> Result +): Result> = + parTraverseResultN(Dispatchers.Default, n, f) + +/** + * Traverses this [Iterable] and runs [f] in [n] parallel operations on [CoroutineContext]. + * If one or more of the [f] returns [Result.failure] then all the [Result.failure] results will be combined using [addSuppressed]. + * + * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [ctx] 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 in parallel. + * + * Cancelling this operation cancels all running tasks. + */ +public suspend fun Iterable.parTraverseResultN( + ctx: CoroutineContext = EmptyCoroutineContext, + n: Int, + f: suspend CoroutineScope.(A) -> Result +): Result> { + val semaphore = Semaphore(n) + return parTraverseResult(ctx) { a -> + semaphore.withPermit { f(a) } + } +} + +/** + * Sequences all tasks in parallel on [ctx] and returns the result. + * If one or more of the tasks returns [Result.failure] then all the [Result.failure] results will be combined using [addSuppressed]. + * + * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [ctx] 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 in parallel. + * + * Cancelling this operation cancels all running tasks. + * + * ```kotlin:ank:playground + * import arrow.core.* + * import arrow.typeclasses.Semigroup + * import arrow.fx.coroutines.* + * import kotlinx.coroutines.Dispatchers + * + * typealias Task = suspend () -> ResultNel + * + * suspend fun main(): Unit { + * //sampleStart + * fun getTask(id: Int): Task = + * suspend { Result.catchNel { println("Working on task $id on ${Thread.currentThread().name}") } } + * + * val res = listOf(1, 2, 3) + * .map(::getTask) + * .parSequenceResult(Dispatchers.IO.nonEmptyList()) + * //sampleEnd + * println(res) + * } + * ``` + */ +@JvmName("parSequenceResultScoped") +public suspend fun Iterable Result>.parSequenceResult( + ctx: CoroutineContext = EmptyCoroutineContext +): Result> = parTraverseResult(ctx) { it() } + +public suspend fun Iterable Result>.parSequenceResult( + ctx: CoroutineContext = EmptyCoroutineContext +): Result> = parTraverseResult(ctx) { it() } + +/** + * Traverses this [Iterable] and runs all mappers [f] on [Dispatchers.Default]. + * If one or more of the [f] returns [Result.failure] then all the [Result.failure] results will be combined using [addSuppressed]. + * + * Cancelling this operation cancels all running tasks. + */ +public suspend fun Iterable.parTraverseResult(f: suspend CoroutineScope.(A) -> Result): Result> = + parTraverseResult(Dispatchers.Default, f) + +/** + * Traverses this [Iterable] and runs all mappers [f] on [CoroutineContext]. + * If one or more of the [f] returns [Result.failure] then all the [Result.failure] results will be combined using [addSuppressed]. + * + * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [ctx] 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 in parallel. + * + * Cancelling this operation cancels all running tasks. + * + * ```kotlin:ank:playground + * import arrow.core.* + * import arrow.typeclasses.Semigroup + * import arrow.fx.coroutines.* + * import kotlinx.coroutines.Dispatchers + * + * object Error + * data class User(val id: Int, val createdOn: String) + * + * suspend fun main(): Unit { + * //sampleStart + * suspend fun getUserById(id: Int): ResultNel = + * if(id % 2 == 0) Error.invalidNel() + * else User(id, Thread.currentThread().name).validNel() + * + * val res = listOf(1, 3, 5) + * .parTraverseResult(Dispatchers.IO.nonEmptyList(), ::getUserById) + * + * val res2 = listOf(1, 2, 3, 4, 5) + * .parTraverseResult(Dispatchers.IO.nonEmptyList(), ::getUserById) + * //sampleEnd + * println(res) + * println(res2) + * } + * ``` + */ +public suspend fun Iterable.parTraverseResult( + ctx: CoroutineContext = EmptyCoroutineContext, + f: suspend CoroutineScope.(A) -> Result +): Result> = + coroutineScope { + map { async(ctx) { f.invoke(this, it) } }.awaitAll().sequenceResult() + } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/ParTraverseValidated.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/ParTraverseValidated.kt index 2ca117693b7..378af49046b 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/ParTraverseValidated.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/ParTraverseValidated.kt @@ -98,11 +98,11 @@ public suspend fun Iterable.parTraverseValidatedN( * * Cancelling this operation cancels all running tasks. */ -@JvmName("parSequenceEitherScoped") -public suspend fun Iterable Validated>.parSequenceEither(semigroup: Semigroup): Validated> = +@JvmName("parSequenceValidatedScoped") +public suspend fun Iterable Validated>.parSequenceValidated(semigroup: Semigroup): Validated> = parTraverseValidated(Dispatchers.Default, semigroup) { it() } -public suspend fun Iterable Validated>.parSequenceEither(semigroup: Semigroup): Validated> = +public suspend fun Iterable Validated>.parSequenceValidated(semigroup: Semigroup): Validated> = parTraverseValidated(Dispatchers.Default, semigroup) { it() } /** diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParTraverseResultTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParTraverseResultTest.kt new file mode 100644 index 00000000000..3b94c934c84 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParTraverseResultTest.kt @@ -0,0 +1,85 @@ +package arrow.fx.coroutines + +import arrow.core.Either +import arrow.core.sequenceResult +import arrow.core.test.generators.result +import io.kotest.matchers.result.shouldBeFailureOfType +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeTypeOf +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.orNull +import io.kotest.property.arbitrary.string +import kotlinx.coroutines.CompletableDeferred + +class ParTraverseResultTest : ArrowFxSpec( + spec = { + "parTraverseResult can traverse effect full computations" { + val ref = Atomic(0) + (0 until 100).parTraverseResult { + Result.success(ref.update { it + 1 }) + } + ref.get() shouldBe 100 + } + + "parTraverseResult runs in parallel" { + val promiseA = CompletableDeferred() + val promiseB = CompletableDeferred() + val promiseC = CompletableDeferred() + + listOf( + suspend { + promiseA.await() + Result.success(promiseC.complete(Unit)) + }, + suspend { + promiseB.await() + Result.success(promiseA.complete(Unit)) + }, + suspend { + promiseB.complete(Unit) + Result.success(promiseC.await()) + } + ).parTraverseResult { it() } + } + + "parTraverseResult results in the correct left" { + checkAll( + Arb.int(min = 10, max = 20), + Arb.int(min = 1, max = 9) + ) { n, killOn -> + (0 until n).parTraverseResult { i -> + if (i == killOn) Result.failure(RuntimeException()) else Result.success(Unit) + }.shouldBeFailureOfType() + } + } + + "parTraverseResult identity is identity" { + checkAll(Arb.list(Arb.result(Arb.int()))) { l -> + val res = l.parTraverseResult { it } + res shouldBe l.sequenceResult() + } + } + + "parTraverseResult results in the correct error" { + checkAll( + Arb.int(min = 10, max = 20), + Arb.int(min = 1, max = 9), + Arb.string().orNull() + ) { n, killOn, msg -> + Either.catch { + (0 until n).parTraverseResult { i -> + if (i == killOn) throw RuntimeException(msg) else Result.success(Unit) + }.let(::println) + }.shouldBeTypeOf>().value.message shouldBe msg + } + } + + "parTraverseResult stack-safe" { + val count = 20_000 + val l = (0 until count).parTraverseResult { Result.success(it) } + l shouldBe Result.success((0 until count).toList()) + } + } +)