From de17cff33f974a5216eae0dbb94e42745cc23f24 Mon Sep 17 00:00:00 2001 From: Andrew Parker Date: Tue, 5 Sep 2023 17:41:15 +0900 Subject: [PATCH] backport traverse functions that arrow deprecated --- lib/api/lib.api | 4 ++ .../cash/quiver/extensions/NonEmptyList.kt | 42 +++++++++++++- .../quiver/extensions/NonEmptyListTest.kt | 57 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/lib/api/lib.api b/lib/api/lib.api index 7e43d38..57f2bd3 100644 --- a/lib/api/lib.api +++ b/lib/api/lib.api @@ -217,6 +217,10 @@ public final class app/cash/quiver/extensions/MapKt { public final class app/cash/quiver/extensions/NonEmptyListKt { public static final fun mapNotNone (Larrow/core/NonEmptyList;Lkotlin/jvm/functions/Function1;)Larrow/core/Option; + public static final fun traverse (Larrow/core/NonEmptyList;Lkotlin/jvm/functions/Function1;)Larrow/core/Either; + public static final fun traverse (Larrow/core/NonEmptyList;Lkotlin/jvm/functions/Function1;)Larrow/core/Option; + public static final fun traverseEither (Larrow/core/NonEmptyList;Lkotlin/jvm/functions/Function1;)Larrow/core/Either; + public static final fun traverseOption (Larrow/core/NonEmptyList;Lkotlin/jvm/functions/Function1;)Larrow/core/Option; } public final class app/cash/quiver/extensions/Nullable { diff --git a/lib/src/main/kotlin/app/cash/quiver/extensions/NonEmptyList.kt b/lib/src/main/kotlin/app/cash/quiver/extensions/NonEmptyList.kt index 1c4d4d5..7636094 100644 --- a/lib/src/main/kotlin/app/cash/quiver/extensions/NonEmptyList.kt +++ b/lib/src/main/kotlin/app/cash/quiver/extensions/NonEmptyList.kt @@ -1,13 +1,53 @@ package app.cash.quiver.extensions +import arrow.core.Either import arrow.core.Nel import arrow.core.NonEmptyList import arrow.core.Option +import arrow.core.raise.either +import arrow.core.raise.option import arrow.core.toNonEmptyListOrNull import arrow.core.toOption +import kotlin.experimental.ExperimentalTypeInference /** - * Applies a function to a NonEmptyList that can result in an optional NonEmptyList + * Applies a function that produces an Option to a NonEmptyList. + * The result is None if the resulting list would be empty, otherwise Some(NonEmptyList). */ inline fun Nel.mapNotNone(f: (A) -> Option): Option> = this.toList().flatMap { a -> f(a).toList() }.toNonEmptyListOrNull().toOption() + +/** + * Map a function that returns an Either across the NonEmptyList. + * + * The first Left result from calling the function will be the result, or if no calls result in a Left + * the result will be a Right(NonEmptyList) of all the B's returned. + */ +@OptIn(ExperimentalTypeInference::class) +@OverloadResolutionByLambdaReturnType +inline fun Nel.traverse(f: (A) -> Either): Either> = + let { nel -> either { nel.map { f(it).bind() } } } + +/** + * Synonym for traverse((A)-> Either): Either> + */ +inline fun Nel.traverseEither(f: (A) -> Either): Either> = + traverse(f) + +/** + * Map a function that returns an Option across the NonEmptyList. + * + * The first None result from calling the function will be the result, or if no calls result in a None + * the result will be a Some(NonEmptyList) of all the B's returned. + */ +@OptIn(ExperimentalTypeInference::class) +@OverloadResolutionByLambdaReturnType +inline fun NonEmptyList.traverse(f: (A) -> Option): Option> = + let { nel -> option { nel.map { f(it).bind() } } } + +/** + * Synonym for traverse((A)-> Option): Option> + */ +inline fun NonEmptyList.traverseOption(f: (A) -> Option): Option> = + traverse(f) + diff --git a/lib/src/test/kotlin/app/cash/quiver/extensions/NonEmptyListTest.kt b/lib/src/test/kotlin/app/cash/quiver/extensions/NonEmptyListTest.kt index d4b973e..f51541e 100644 --- a/lib/src/test/kotlin/app/cash/quiver/extensions/NonEmptyListTest.kt +++ b/lib/src/test/kotlin/app/cash/quiver/extensions/NonEmptyListTest.kt @@ -1,8 +1,15 @@ package app.cash.quiver.extensions +import arrow.core.None import arrow.core.Option +import arrow.core.left +import arrow.core.nel import arrow.core.nonEmptyListOf +import arrow.core.right import arrow.core.some +import io.kotest.assertions.arrow.core.shouldBeLeft +import io.kotest.assertions.arrow.core.shouldBeNone +import io.kotest.assertions.arrow.core.shouldBeRight import io.kotest.assertions.arrow.core.shouldBeSome import io.kotest.core.spec.style.StringSpec import kotlin.coroutines.resume @@ -10,10 +17,60 @@ import kotlin.coroutines.suspendCoroutine class NonEmptyListTest : StringSpec({ + "mapNotNone should map across the NonEmptyList" { + nonEmptyListOf(1, 2, 3).mapNotNone { + it.some() + }.shouldBeSome(nonEmptyListOf(1, 2, 3)) + } + "mapNotNone should allow for suspended functions" { suspend fun intToOption(i: Int): Option = suspendCoroutine { it.resume(i.some()) } nonEmptyListOf(1, 2, 3).mapNotNone { intToOption(it) }.shouldBeSome(nonEmptyListOf(1, 2, 3)) } + + "mapNotNone should skip None results" { + nonEmptyListOf(1, 2, 3).mapNotNone { + if (it % 2 == 0) { + None + } else { + it.some() + } + }.shouldBeSome(nonEmptyListOf(1, 3)) + } + + "mapNotNone should return None if all results are None" { + nonEmptyListOf(1, 2, 3).mapNotNone { + None + }.shouldBeNone() + } + + "traverseEither maps across the NonEmptyList" { + nonEmptyListOf(1,2,3).traverseEither { + (it.toString()).right() + }.shouldBeRight(nonEmptyListOf("1", "2", "3")) + } + + "traverseEither short-circuits on Left" { + nonEmptyListOf(1,2,3,4).traverseEither { + if (it % 2 == 0) + it.toString().left() + else it.right() + }.shouldBeLeft("2") + } + + "traverseOption maps Option across the NonEmptyList" { + nonEmptyListOf(1,2,3).traverseOption { + (it.toString()).some() + }.shouldBeSome(nonEmptyListOf("1", "2", "3")) + } + + "traverseOption short-circuits on None" { + nonEmptyListOf(1,2,3,4).traverseOption { + if (it % 2 == 0) + None + else it.some() + }.shouldBeNone() + } })