diff --git a/arrow-libs/core/arrow-core/api/arrow-core.api b/arrow-libs/core/arrow-core/api/arrow-core.api index 252e60ea971..4270ce4bbf0 100644 --- a/arrow-libs/core/arrow-core/api/arrow-core.api +++ b/arrow-libs/core/arrow-core/api/arrow-core.api @@ -3250,6 +3250,7 @@ public final class arrow/core/raise/DefaultRaise : arrow/core/raise/Raise { public fun bind (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bindAll (Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public fun bindAll (Ljava/lang/Iterable;)Ljava/util/List; public fun bindAll (Ljava/util/Map;)Ljava/util/Map; public fun catch (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -3274,8 +3275,10 @@ public final class arrow/core/raise/IorRaise : arrow/core/raise/Raise { public fun bind (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bindAll (Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public fun bindAll (Ljava/lang/Iterable;)Ljava/util/List; public fun bindAll (Ljava/util/Map;)Ljava/util/Map; + public final fun bindAllIor (Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public final fun bindAllIor (Ljava/lang/Iterable;)Ljava/util/List; public final fun bindAllIor (Ljava/util/Map;)Ljava/util/Map; public fun catch (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -3296,6 +3299,7 @@ public final class arrow/core/raise/NullableRaise : arrow/core/raise/Raise { public final fun bind (Ljava/lang/Object;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bindAll (Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public fun bindAll (Ljava/lang/Iterable;)Ljava/util/List; public fun bindAll (Ljava/util/Map;)Ljava/util/Map; public final fun bindAllNullable (Ljava/lang/Iterable;)Ljava/util/List; @@ -3321,8 +3325,10 @@ public final class arrow/core/raise/OptionRaise : arrow/core/raise/Raise { public fun bind (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bindAll (Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public fun bindAll (Ljava/lang/Iterable;)Ljava/util/List; public fun bindAll (Ljava/util/Map;)Ljava/util/Map; + public final fun bindAllOption (Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public final fun bindAllOption (Ljava/lang/Iterable;)Ljava/util/List; public final fun bindAllOption (Ljava/util/Map;)Ljava/util/Map; public fun catch (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -3344,6 +3350,7 @@ public abstract interface class arrow/core/raise/Raise { public abstract fun bind (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun bind (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public abstract fun bind (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun bindAll (Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public abstract fun bindAll (Ljava/lang/Iterable;)Ljava/util/List; public abstract fun bindAll (Ljava/util/Map;)Ljava/util/Map; public abstract fun catch (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -3361,6 +3368,7 @@ public final class arrow/core/raise/Raise$DefaultImpls { public static fun bind (Larrow/core/raise/Raise;Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun bind (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public static fun bind (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun bindAll (Larrow/core/raise/Raise;Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public static fun bindAll (Larrow/core/raise/Raise;Ljava/lang/Iterable;)Ljava/util/List; public static fun bindAll (Larrow/core/raise/Raise;Ljava/util/Map;)Ljava/util/Map; public static fun catch (Larrow/core/raise/Raise;Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -3378,6 +3386,7 @@ public class arrow/core/raise/RaiseAccumulate : arrow/core/raise/Raise { public fun bind (Larrow/core/continuations/Effect;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bindAll (Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public fun bindAll (Ljava/lang/Iterable;)Ljava/util/List; public fun bindAll (Ljava/util/Map;)Ljava/util/Map; public final fun bindNel (Larrow/core/Either;)Ljava/lang/Object; @@ -3473,8 +3482,10 @@ public final class arrow/core/raise/ResultRaise : arrow/core/raise/Raise { public final fun bind (Ljava/lang/Object;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun bind (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun bindAll (Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public fun bindAll (Ljava/lang/Iterable;)Ljava/util/List; public fun bindAll (Ljava/util/Map;)Ljava/util/Map; + public final fun bindAllResult (Larrow/core/NonEmptyList;)Larrow/core/NonEmptyList; public final fun bindAllResult (Ljava/lang/Iterable;)Ljava/util/List; public final fun bindAllResult (Ljava/util/Map;)Ljava/util/Map; public fun catch (Larrow/core/continuations/Effect;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt index d4821c2e8f5..120ed3b08bc 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Builders.kt @@ -8,6 +8,7 @@ import arrow.atomic.Atomic import arrow.atomic.updateAndGet import arrow.core.Either import arrow.core.Ior +import arrow.core.NonEmptyList import arrow.core.None import arrow.core.Option import arrow.core.Some @@ -84,6 +85,11 @@ public class ResultRaise(private val raise: Raise) : Raise @JvmName("bindAllResult") public fun Iterable>.bindAll(): List = map { it.bind() } + + @RaiseDSL + @JvmName("bindAllResult") + public fun NonEmptyList>.bindAll(): NonEmptyList = + map { it.bind() } } public class OptionRaise(private val raise: Raise) : Raise by raise { @@ -99,6 +105,11 @@ public class OptionRaise(private val raise: Raise) : Raise by raise public fun Iterable>.bindAll(): List = map { it.bind() } + @RaiseDSL + @JvmName("bindAllOption") + public fun NonEmptyList>.bindAll(): NonEmptyList = + map { it.bind() } + @RaiseDSL public fun ensure(value: Boolean): Unit = ensure(value) { None } @@ -123,6 +134,11 @@ public class IorRaise @PublishedApi internal constructor( public fun Iterable>.bindAll(): List = map { it.bind() } + @RaiseDSL + @JvmName("bindAllIor") + public fun NonEmptyList>.bindAll(): NonEmptyList = + map { it.bind() } + @RaiseDSL public fun Ior.bind(): A = when (this) { diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt index 94d029d1369..cdc3d6ecb3b 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/Raise.kt @@ -6,6 +6,7 @@ package arrow.core.raise import arrow.core.Either +import arrow.core.NonEmptyList import arrow.core.Validated import arrow.core.ValidatedDeprMsg import arrow.core.continuations.EffectScope @@ -249,6 +250,10 @@ public interface Raise { @RaiseDSL public fun Iterable>.bindAll(): List = map { it.bind() } + + @RaiseDSL + public fun NonEmptyList>.bindAll(): NonEmptyList = + map { it.bind() } } /** diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt index d52283e90a7..7b5deb84853 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt @@ -502,6 +502,9 @@ public open class RaiseAccumulate( override fun Iterable>.bindAll(): List = mapOrAccumulate { it.bind() } + override fun NonEmptyList>.bindAll(): NonEmptyList = + requireNotNull(mapOrAccumulate { it.bind() }.toNonEmptyListOrNull()) + @RaiseDSL public fun EitherNel.bindNel(): A = when (this) { is Either.Left -> raise.raise(value) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/NonEmptyListTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/NonEmptyListTest.kt index aab8cbdd9ac..3e29760291b 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/NonEmptyListTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/NonEmptyListTest.kt @@ -2,16 +2,21 @@ package arrow.core import arrow.core.test.laws.SemigroupLaws import arrow.core.test.nonEmptyList +import arrow.core.test.stackSafeIteration import arrow.core.test.testLaws import arrow.typeclasses.Semigroup import io.kotest.assertions.withClue import io.kotest.core.spec.style.StringSpec +import io.kotest.inspectors.shouldForAll import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.property.Arb import io.kotest.matchers.shouldBe import io.kotest.property.arbitrary.boolean import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.negativeInt +import io.kotest.property.arbitrary.pair +import io.kotest.property.arbitrary.string import io.kotest.property.checkAll import kotlin.math.max import kotlin.math.min @@ -32,15 +37,21 @@ class NonEmptyListTest : StringSpec({ } } + "flatten" { + checkAll(Arb.nonEmptyList(Arb.int())) { nel -> + nonEmptyListOf(nel).flatten() shouldBe nel + } + } + "traverse for Either stack-safe" { // also verifies result order and execution order (l to r) val acc = mutableListOf() - val res = (0..20_000).toNonEmptyListOrNull()?.traverse { a -> + val res = (0..stackSafeIteration()).toNonEmptyListOrNull()?.traverse { a -> acc.add(a) Either.Right(a) } res shouldBe Either.Right(acc.toNonEmptyListOrNull()) - res shouldBe Either.Right((0..20_000).toNonEmptyListOrNull()) + res shouldBe Either.Right((0..stackSafeIteration()).toNonEmptyListOrNull()) } "traverse for Either short-circuit" { @@ -62,29 +73,31 @@ class NonEmptyListTest : StringSpec({ "sequence for Either should be consistent with traverseEither" { checkAll(Arb.nonEmptyList(Arb.int())) { ints -> - ints.map { if (it % 2 == 0) Either.Right(it) else Either.Left(it) }.sequence() shouldBe - ints.traverse { if (it % 2 == 0) Either.Right(it) else Either.Left(it) } + fun onlyEven(i: Int) = if (i % 2 == 0) Either.Right(i) else Either.Left(i) + ints.map { onlyEven(it) }.sequence() shouldBe ints.traverse { onlyEven(it) } } } "traverse for Option is stack-safe" { // also verifies result order and execution order (l to r) val acc = mutableListOf() - val res = (0..20_000).toNonEmptyListOrNull()?.traverse { a -> + val res = (0..stackSafeIteration()).toNonEmptyListOrNull()?.traverse { a -> acc.add(a) Some(a) } res shouldBe Some(acc.toNonEmptyListOrNull()) - res shouldBe Some((0..20_000).toNonEmptyListOrNull()) + res shouldBe Some((0..stackSafeIteration()).toNonEmptyListOrNull()) } "traverse for Option short-circuits" { checkAll(Arb.nonEmptyList(Arb.int())) { ints -> val acc = mutableListOf() - val evens = ints.traverse { - (it % 2 == 0).maybe { - acc.add(it) - it + val evens = ints.traverse { a -> + if ((a % 2 == 0)) { + acc.add(a) + Some(a) + } else { + None } } acc shouldBe ints.takeWhile { it % 2 == 0 } @@ -94,32 +107,49 @@ class NonEmptyListTest : StringSpec({ "sequence for Option yields some when all entries in the list are some" { checkAll(Arb.nonEmptyList(Arb.int())) { ints -> - val evens = ints.map { (it % 2 == 0).maybe { it } }.sequence() + val evens = ints.map { a -> + if ((a % 2 == 0)) { + Some(a) + } else { + None + } + }.sequence() evens.fold({ Unit }) { it shouldBe ints } } } "sequence for Option should be consistent with traverseOption" { checkAll(Arb.nonEmptyList(Arb.int())) { ints -> - ints.map { (it % 2 == 0).maybe { it } }.sequence() shouldBe - ints.traverse { (it % 2 == 0).maybe { it } } + ints.map { a-> + if ((a % 2 == 0)) { + Some(a) + } else { + None + } + }.sequence() shouldBe + ints.traverse { a-> + if ((a % 2 == 0)) { + Some(a) + } else { + None + } + } } } - "traverse for Validated stack-safe" { + "traverse for Validated is stack-safe" { // also verifies result order and execution order (l to r) val acc = mutableListOf() - val res = (0..20_000) - .toNonEmptyListOrNull()!! - .traverse(Semigroup.string()) { + val res = (0..stackSafeIteration()) + .toNonEmptyListOrNull()?.traverse(Semigroup.string()) { acc.add(it) - Validated.Valid(it) - } + Validated.Valid(it) + } res shouldBe Validated.Valid(acc) - res shouldBe Validated.Valid((0..20_000).toList()) + res shouldBe Validated.Valid((0..stackSafeIteration()).toList()) } - "traverse for Validated acummulates" { + "traverse for Validated accumulates" { checkAll(Arb.nonEmptyList(Arb.int())) { ints -> val res: ValidatedNel> = ints.traverse(Semigroup.nonEmptyList()) { i: Int -> if (i % 2 == 0) i.validNel() else i.invalidNel() } @@ -133,13 +163,99 @@ class NonEmptyListTest : StringSpec({ "can align lists with different lengths" { checkAll(Arb.nonEmptyList(Arb.boolean()), Arb.nonEmptyList(Arb.boolean())) { a, b -> - a.align(b).size shouldBe max(a.size, b.size) + val result = a.align(b) + + result.size shouldBe max(a.size, b.size) + result.take(min(a.size, b.size)).shouldForAll { + it.isBoth() shouldBe true + } + result.drop(min(a.size, b.size)).shouldForAll { + if (a.size < b.size) { + it.isRight() shouldBe true + } else { + it.isLeft() shouldBe true + } + } } + } - checkAll(Arb.nonEmptyList(Arb.boolean()), Arb.nonEmptyList(Arb.boolean())) { a, b -> - a.align(b).all.take(min(a.size, b.size)).forEach { - it.isBoth shouldBe true + "mapOrAccumulate is stack-safe, and runs in original order" { + val acc = mutableListOf() + val res = (0..stackSafeIteration()) + .toNonEmptyListOrNull()!! + .mapOrAccumulate(String::plus) { + acc.add(it) + it + } + res shouldBe Either.Right(acc) + res shouldBe Either.Right((0..stackSafeIteration()).toList()) + } + + "mapOrAccumulate accumulates errors" { + checkAll(Arb.nonEmptyList(Arb.int())) { nel -> + val res = nel.mapOrAccumulate { i -> + if (i % 2 == 0) i else raise(i) + } + + val expected = nel.filterNot { it % 2 == 0 } + .toNonEmptyListOrNull()?.left() ?: nel.filter { it % 2 == 0 }.right() + + res shouldBe expected + } + } + + "mapOrAccumulate accumulates errors with combine function" { + checkAll(Arb.nonEmptyList(Arb.negativeInt())) { nel -> + val res = nel.mapOrAccumulate(String::plus) { i -> + if (i > 0) i else raise("Negative") } + + res shouldBe nel.map { "Negative" }.joinToString("").left() + } + } + + "padZip" { + checkAll(Arb.nonEmptyList(Arb.int()), Arb.nonEmptyList(Arb.int())) { a, b -> + val result = a.padZip(b) + val left = a + List(max(0, b.size - a.size)) { null } + val right = b + List(max(0, a.size - b.size)) { null } + + result shouldBe left.zip(right) + } + } + + "padZip with transformation" { + checkAll(Arb.nonEmptyList(Arb.int()), Arb.nonEmptyList(Arb.int())) { a, b -> + val result = a.padZip(b, { it * 2 }, { it * 3 }, { x, y -> x + y }) + + val minSize = min(a.size, b.size) + result.size shouldBe max(a.size, b.size) + result.take(minSize) shouldBe a.take(minSize).zip(b.take(minSize)) { x, y -> x + y } + + if (a.size > b.size) + result.drop(minSize) shouldBe a.drop(minSize).map { it * 2 } + else + result.drop(minSize) shouldBe b.drop(minSize).map { it * 3 } + } + } + + "unzip is the inverse of zip" { + checkAll(Arb.nonEmptyList(Arb.int())) { nel -> + val zipped = nel.zip(nel) + val left = zipped.map { it.first } + val right = zipped.map { it.second } + + left shouldBe nel + right shouldBe nel + } + } + + "unzip with split function" { + checkAll(Arb.nonEmptyList(Arb.pair(Arb.int(), Arb.string()))) { nel -> + val unzipped = nel.unzip(::identity) + + unzipped.first shouldBe nel.map { it.first } + unzipped.second shouldBe nel.map { it.second } } } diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Platform.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Platform.kt index 9ebe272d94d..a308c4f8ad5 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Platform.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Platform.kt @@ -4,6 +4,6 @@ import io.kotest.common.Platform import io.kotest.common.platform fun stackSafeIteration(): Int = when (platform) { - Platform.JVM -> 500_000 + Platform.JVM -> 200_000 else -> 1000 }