Skip to content

Commit

Permalink
Backport mapOrAccumulate (#2922)
Browse files Browse the repository at this point in the history
  • Loading branch information
nomisRev authored Feb 8, 2023
1 parent bb059e7 commit 897b84c
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 1 deletion.
2 changes: 2 additions & 0 deletions arrow-libs/core/arrow-core/api/arrow-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,8 @@ public final class arrow/core/IterableKt {
public static final fun lastOrNone (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Larrow/core/Option;
public static final fun leftPadZip (Ljava/lang/Iterable;Ljava/lang/Iterable;)Ljava/util/List;
public static final fun leftPadZip (Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)Ljava/util/List;
public static final fun mapOrAccumulate (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)Larrow/core/Either;
public static final fun mapOrAccumulate (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/core/Either;
public static final fun padZip (Ljava/lang/Iterable;Ljava/lang/Iterable;)Ljava/util/List;
public static final fun padZip (Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)Ljava/util/List;
public static final fun prependTo (Ljava/lang/Object;Ljava/lang/Iterable;)Ljava/util/List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package arrow.core

import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.raise.Raise
import arrow.core.raise.either
import arrow.typeclasses.Monoid
import arrow.typeclasses.Semigroup
import kotlin.Result.Companion.success
Expand Down Expand Up @@ -428,6 +430,57 @@ public inline fun <A, B> Iterable<A>.traverse(f: (A) -> B?): List<B>? {
public fun <A> Iterable<A?>.sequenceNullable(): List<A>? =
sequence()

/**
* Returns [Either] a [List] containing the results of applying the given [transform] function
* to each element in the original collection,
* **or** accumulate all the _logical errors_ that were _raised_ while transforming the collection.
* The [combine] function is used to accumulate all the _logical errors_.
*/
@OptIn(ExperimentalTypeInference::class)
public inline fun <Error, A, B> Iterable<A>.mapOrAccumulate(
combine: (Error, Error) -> Error,
@BuilderInference transform: Raise<Error>.(A) -> B,
): Either<Error, List<B>> =
fold<A, Either<Error, ArrayList<B>>>(Right(ArrayList(collectionSizeOrDefault(10)))) { acc, a ->
when (val res = either { transform(a) }) {
is Right -> when (acc) {
is Right -> acc.also { acc.value.add(res.value) }
is Left -> acc
}

is Left -> when (acc) {
is Right -> res
is Left -> Left(combine(acc.value, res.value))
}
}
}

/**
* Returns [Either] a [List] containing the results of applying the given [transform] function
* to each element in the original collection,
* **or** accumulate all the _logical errors_ into a [NonEmptyList] that were _raised_ while applying the [transform] function.
*/
@OptIn(ExperimentalTypeInference::class)
public inline fun <Error, A, B> Iterable<A>.mapOrAccumulate(
@BuilderInference transform: Raise<Error>.(A) -> B,
): Either<NonEmptyList<Error>, List<B>> {
val buffer = mutableListOf<Error>()
val res = fold<A, Either<MutableList<Error>, ArrayList<B>>>(Right(ArrayList(collectionSizeOrDefault(10)))) { acc, a ->
when (val res = either { transform(a) }) {
is Right -> when (acc) {
is Right -> acc.also { acc.value.add(res.value) }
is Left -> acc
}

is Left -> when (acc) {
is Right -> Left(buffer.also { it.add(res.value) })
is Left -> Left(buffer.also { it.add(res.value) })
}
}
}
return res.mapLeft { NonEmptyList(it[0], it.drop(1)) }
}

/**
* Flatten a list of [Either] into a single [Either] with a list of values, or accumulates all errors using [combine].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class IterableTest : StringSpec({
.fold("") { acc, either -> "$acc${either.value}" }.left()
else list.filterIsInstance<Either.Right<Int>>().map { it.value }.right()

list.flattenOrAccumulate { a, b -> "$a$b" } shouldBe expected
list.flattenOrAccumulate(String::plus) shouldBe expected
}
}

Expand All @@ -43,6 +43,34 @@ class IterableTest : StringSpec({
}
}

"mapAccumulating stack-safe, and runs in original order" {
val acc = mutableListOf<Int>()
val res = (0..20_000).mapOrAccumulate(String::plus) {
acc.add(it)
it
}
res shouldBe acc.right()
res shouldBe (0..20_000).toList().right()
}

"mapAccumulating accumulates" {
checkAll(Arb.list(Arb.int())) { ints ->
val res: Either<NonEmptyList<Int>, List<Int>> =
ints.mapOrAccumulate { i -> if (i % 2 == 0) i else raise(i) }

val expected: Either<NonEmptyList<Int>, List<Int>> = ints.filterNot { it % 2 == 0 }
.toNonEmptyListOrNull()?.left() ?: ints.filter { it % 2 == 0 }.right()

res shouldBe expected
}
}

"mapAccumulating with String::plus" {
listOf(1, 2, 3).mapOrAccumulate(String::plus) { i ->
raise("fail")
} shouldBe Either.Left("failfailfail")
}

"traverse Either stack-safe" {
// also verifies result order and execution order (l to r)
val acc = mutableListOf<Int>()
Expand Down

0 comments on commit 897b84c

Please sign in to comment.