Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce traverse and sequence for nullable types #2519

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,9 @@ public sealed class Either<out A, out B> {
public inline fun <C> traverseOption(fa: (B) -> Option<C>): Option<Either<A, C>> =
fold({ None }, { right -> fa(right).map { Right(it) } })

public inline fun <C> traverseNullable(fa: (B) -> C?): Either<A, C>? =
fold({ null }, { right -> fa(right)?.let { Right(it) } })

public inline fun <AA, C> traverseValidated(fa: (B) -> Validated<AA, C>): Validated<AA, Either<A, C>> =
when (this) {
is Right -> fa(this.value).map { Right(it) }
Expand All @@ -928,6 +931,9 @@ public sealed class Either<out A, out B> {
public inline fun <AA, C> bitraverseOption(fl: (A) -> Option<AA>, fr: (B) -> Option<C>): Option<Either<AA, C>> =
fold({ fl(it).map(::Left) }, { fr(it).map(::Right) })

public inline fun <AA, C> bitraverseNullable(fl: (A) -> AA?, fr: (B) -> C?): Either<AA, C>? =
fold({ fl(it)?.let(::Left) }, { fr(it)?.let(::Right) })

public inline fun <AA, C, D> bitraverseValidated(
fe: (A) -> Validated<AA, C>,
fa: (B) -> Validated<AA, D>
Expand Down Expand Up @@ -1535,6 +1541,9 @@ public fun <A, B> Either<A, Iterable<B>>.sequence(): List<Either<A, B>> =
public fun <A, B> Either<A, Option<B>>.sequenceOption(): Option<Either<A, B>> =
traverseOption(::identity)

public fun <A, B> Either<A, B?>.sequenceNullable(): Either<A, B>? =
traverseNullable(::identity)

public fun <A, B, C> Either<A, Validated<B, C>>.sequenceValidated(): Validated<B, Either<A, C>> =
traverseValidated(::identity)

Expand All @@ -1544,5 +1553,8 @@ public fun <A, B> Either<Iterable<A>, Iterable<B>>.bisequence(): List<Either<A,
public fun <A, B> Either<Option<A>, Option<B>>.bisequenceOption(): Option<Either<A, B>> =
bitraverseOption(::identity, ::identity)

public fun <A, B> Either<A?, B?>.bisequenceNullable(): Either<A, B>? =
bitraverseNullable(::identity, ::identity)

public fun <A, B, C> Either<Validated<A, B>, Validated<A, C>>.bisequenceValidated(): Validated<A, Either<B, C>> =
bitraverseValidated(::identity, ::identity)
23 changes: 23 additions & 0 deletions arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Ior.kt
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,16 @@ public sealed class Ior<out A, out B> {
{ a, b -> fa(a).zip(fb(b)) { aa, c -> Both(aa, c) } }
)

public inline fun <C, D> bitraverseNullable(
fa: (A) -> C?,
fb: (B) -> D?
): Ior<C, D>? =
fold(
{ a -> fa(a)?.let { Left(it) } },
{ b -> fb(b)?.let { Right(it) } },
{ a, b -> Nullable.zip(fa(a), fb(b)) { aa, c -> Both(aa, c) } }
)

public inline fun <AA, C, D> bitraverseValidated(
SA: Semigroup<AA>,
fa: (A) -> Validated<AA, C>,
Expand Down Expand Up @@ -515,6 +525,13 @@ public sealed class Ior<out A, out B> {
{ a, b -> fa(b).map { Both(a, it) } }
)

public inline fun <C> traverseNullable(fa: (B) -> C?): Ior<A, C>? =
fold(
{ a -> Left(a) },
{ b -> fa(b)?.let { Right(it) } },
{ a, b -> fa(b)?.let { Both(a, it) } }
)

public inline fun <AA, C> traverseValidated(fa: (B) -> Validated<AA, C>): Validated<AA, Ior<A, C>> =
fold(
{ a -> Valid(Left(a)) },
Expand Down Expand Up @@ -562,6 +579,9 @@ public fun <A, B, C> Ior<Either<A, B>, Either<A, C>>.bisequenceEither(): Either<
public fun <B, C> Ior<Option<B>, Option<C>>.bisequenceOption(): Option<Ior<B, C>> =
bitraverseOption(::identity, ::identity)

public fun <B, C> Ior<B?, C?>.bisequenceNullable(): Ior<B, C>? =
bitraverseNullable(::identity, ::identity)

public fun <A, B, C> Ior<Validated<A, B>, Validated<A, C>>.bisequenceValidated(SA: Semigroup<A>): Validated<A, Ior<B, C>> =
bitraverseValidated(SA, ::identity, ::identity)

Expand Down Expand Up @@ -623,6 +643,9 @@ public fun <A, B, C> Ior<A, Either<B, C>>.sequenceEither(): Either<B, Ior<A, C>>
public fun <A, B> Ior<A, Option<B>>.sequenceOption(): Option<Ior<A, B>> =
traverseOption(::identity)

public fun <A, B> Ior<A, B?>.sequenceNullable(): Ior<A, B>? =
traverseNullable(::identity)

public fun <A, B, C> Ior<A, Validated<B, C>>.sequenceValidated(): Validated<B, Ior<A, C>> =
traverseValidated(::identity)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,22 @@ public inline fun <A, B> Iterable<A>.traverseOption(f: (A) -> Option<B>): Option
public fun <A> Iterable<Option<A>>.sequenceOption(): Option<List<A>> =
this.traverseOption { it }

public inline fun <A, B> Iterable<A>.traverseNullable(f: (A) -> B?): List<B>? {
val acc = mutableListOf<B>()
forEach { a ->
val res = f(a)
if (res != null) {
acc.add(res)
} else {
return res
}
}
return acc.toList()
}

public fun <A> Iterable<A?>.sequenceNullable(): List<A>? =
this.traverseNullable { it }

public fun <A> Iterable<A>.void(): List<Unit> =
map { }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,12 @@ public sealed class Validated<out E, out A> {
is Invalid -> None
}

public inline fun <B> traverseNullable(fa: (A) -> B?): Validated<E, B>? =
when (this) {
is Valid -> fa(this.value)?.let { Valid(it) }
is Invalid -> null
}

public inline fun <B> bifoldLeft(
c: B,
fe: (B, E) -> B,
Expand All @@ -592,6 +598,12 @@ public sealed class Validated<out E, out A> {
): Option<Validated<B, C>> =
fold({ fe(it).map(::Invalid) }, { fa(it).map(::Valid) })

public inline fun <B, C> bitraverseNullable(
fe: (E) -> B?,
fa: (A) -> C?
): Validated<B, C>? =
fold({ fe(it)?.let(::Invalid) }, { fa(it)?.let(::Valid) })

public inline fun <B> foldMap(MB: Monoid<B>, f: (A) -> B): B =
fold({ MB.empty() }, f)

Expand Down Expand Up @@ -1076,6 +1088,9 @@ public fun <E, A, B> Validated<Either<E, A>, Either<E, B>>.bisequenceEither(): E
public fun <A, B> Validated<Option<A>, Option<B>>.bisequenceOption(): Option<Validated<A, B>> =
bitraverseOption(::identity, ::identity)

public fun <A, B> Validated<A?, B?>.bisequenceNullable(): Validated<A, B>? =
bitraverseNullable(::identity, ::identity)

public fun <E, A> Validated<E, A>.fold(MA: Monoid<A>): A = MA.run {
foldLeft(empty()) { acc, a -> acc.combine(a) }
}
Expand All @@ -1092,6 +1107,9 @@ public fun <E, A, B> Validated<A, Either<E, B>>.sequenceEither(): Either<E, Vali
public fun <A, B> Validated<A, Option<B>>.sequenceOption(): Option<Validated<A, B>> =
traverseOption(::identity)

public fun <A, B> Validated<A, B?>.sequenceNullable(): Validated<A, B>? =
traverseNullable(::identity)

public operator fun <E : Comparable<E>, A : Comparable<A>> Validated<E, A>.compareTo(other: Validated<E, A>): Int =
fold(
{ l1 -> other.fold({ l2 -> l1.compareTo(l2) }, { -1 }) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,22 @@ class EitherTest : UnitSpec() {
}
}

"traverseNullable should return non-nullable if either is right" {
val right: Either<String, Int> = Right(1)
val left: Either<String, Int> = Left("foo")

right.traverseNullable { it } shouldBe Right(1)
right.traverseNullable { null } shouldBe null
left.traverseNullable { it } shouldBe null
}

"sequenceNullable should be consistent with traverseNullable" {
checkAll(Arb.either(Arb.string(), Arb.int())) { either ->
either.map { it }.sequenceNullable() shouldBe either.traverseNullable { it }
either.map { null }.sequenceNullable() shouldBe null
}
}

"traverseOption should return option if either is right" {
val right: Either<String, Int> = Right(1)
val left: Either<String, Int> = Left("foo")
Expand Down Expand Up @@ -517,6 +533,24 @@ class EitherTest : UnitSpec() {
}
}

"bitraverseNullable should wrap either in a nullable" {
val right: Either<String, Int> = Right(1)
val left: Either<String, Int> = Left("foo")

right.bitraverseNullable({ it }, { it.toString() }) shouldBe Right("1")
left.bitraverseNullable({ it }, { it.toString() }) shouldBe Left("foo")

right.bitraverseNullable({ it }, { null }) shouldBe null
left.bitraverseNullable({ null }, { it.toString() }) shouldBe null
}

"bisequenceNullable should be consistent with bitraverseNullable" {
checkAll(Arb.either(Arb.string(), Arb.int())) { either ->
either.bimap({ it }, { it }).bisequenceNullable() shouldBe
either.bitraverseNullable({ it }, { it })
}
}

"bitraverseOption should wrap either in an option" {
val right: Either<String, Int> = Right(1)
val left: Either<String, Int> = Left("foo")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,29 @@ class IorTest : UnitSpec() {
}
}

"traverseNullable should wrap ior in a nullable" {
checkAll(Arb.int(), Arb.string()) { a: Int, b: String ->
val iorL: Ior<Int, String> = a.leftIor()
val iorR: Ior<Int, String> = b.rightIor()
val iorBoth: Ior<Int, String> = (a to b).bothIor()

iorL.traverseNullable { it } shouldBe Ior.Left(a)
iorR.traverseNullable { it } shouldBe Ior.Right(b)
iorBoth.traverseNullable { it } shouldBe Ior.Both(a, b)

iorL.traverseNullable { null } shouldBe Ior.Left(a)
iorR.traverseNullable { null } shouldBe null
iorBoth.traverseNullable { null } shouldBe null
}
}

"sequenceNullable should be consistent with traverseNullable" {
checkAll(Arb.ior(Arb.int(), Arb.string())) { ior ->
ior.map<String?> { it }.sequenceNullable() shouldBe ior.traverseNullable { it }
ior.map<String?> { null }.sequenceNullable() shouldBe ior.traverseNullable { null }
}
}

"traverseOption should wrap ior in an Option" {
checkAll(Arb.int(), Arb.string()) { a: Int, b: String ->
val iorL: Ior<Int, String> = a.leftIor()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package arrow.core
import arrow.core.test.UnitSpec
import arrow.core.test.generators.option
import arrow.typeclasses.Semigroup
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.property.Arb
import io.kotest.property.checkAll
import io.kotest.matchers.shouldBe
Expand Down Expand Up @@ -120,6 +123,59 @@ class IterableTest : UnitSpec() {
}
}

"traverseNullable is stack-safe" {
// also verifies result order and execution order (l to r)
val acc = mutableListOf<Int?>()
val res = (0..20_000).traverseNullable { a ->
acc.add(a)
a
}
res.shouldNotBeNull() shouldBe acc
res.shouldNotBeNull() shouldBe (0..20_000).toList()
}

"traverseNullable short-circuits" {
checkAll(Arb.list(Arb.int())) { ints ->
val acc = mutableListOf<Int>()
val evens = ints.traverseNullable {
if (it % 2 == 0) {
acc.add(it)
it
} else {
null
}
}

val expected = ints.takeWhile { it % 2 == 0 }
acc shouldBe expected

if (ints.any { it % 2 != 0 }) {
evens.shouldBeNull()
} else {
evens.shouldNotBeNull() shouldContainExactly expected
}
}
}

"sequenceNullable yields some when all entries in the list are not null" {
checkAll(Arb.list(Arb.int())) { ints ->
val evens = ints.map { if (it % 2 == 0) it else null }.sequenceNullable()

val expected = ints.takeWhile { it % 2 == 0 }
if (ints.any { it % 2 != 0 }) {
evens.shouldBeNull()
} else {
evens.shouldNotBeNull() shouldContainExactly ints.takeWhile { it % 2 == 0 }
}
}
}

"sequenceNullable should be consistent with traversNullable" {
checkAll(Arb.list(Arb.int())) { ints ->
ints.map { it as Int? }.sequenceNullable() shouldBe ints.traverseNullable { it as Int? }
}
}

"traverseValidated stack-safe" {
// also verifies result order and execution order (l to r)
val acc = mutableListOf<Int>()
Expand All @@ -133,8 +189,9 @@ class IterableTest : UnitSpec() {

"traverseValidated acumulates" {
checkAll(Arb.list(Arb.int())) { ints ->
val res: ValidatedNel<Int, List<Int>> = ints.map { i -> if (i % 2 == 0) Valid(i) else Invalid(nonEmptyListOf(i)) }
.sequenceValidated()
val res: ValidatedNel<Int, List<Int>> =
ints.map { i -> if (i % 2 == 0) Valid(i) else Invalid(nonEmptyListOf(i)) }
.sequenceValidated()

val expected: ValidatedNel<Int, List<Int>> = NonEmptyList.fromList(ints.filterNot { it % 2 == 0 })
.fold({ Valid(ints.filter { it % 2 == 0 }) }, { Invalid(it) })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import arrow.typeclasses.Monoid
import arrow.typeclasses.Semigroup
import arrow.core.test.generators.validated
import io.kotest.assertions.fail
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.property.Arb
import io.kotest.property.checkAll
import io.kotest.matchers.shouldBe
Expand Down Expand Up @@ -374,7 +375,27 @@ class ValidatedTest : UnitSpec() {
val invalid = Invalid(b)

valid.traverseOption { Some(it) } shouldBe valid.map { Some(it) }.sequenceOption()
invalid.traverseOption { Some(it) } shouldBe invalid.map { Some(it) }.sequenceOption()
invalid.traverseOption { Some(it) } shouldBe invalid.map { Some(it) }.sequenceOption()
}
}

"traverseNullable should yield non-null object when validated is valid" {
val valid = Valid("Who")
val invalid = Invalid("Nope")

valid.traverseNullable<String?> { it } shouldBe Valid("Who")
valid.traverseNullable<String?> { null }.shouldBeNull()
invalid.traverseNullable<String?> { it }.shouldBeNull()
}

"sequenceNullable should yield consistent result with traverseNullable" {
checkAll(Arb.string(), Arb.string()) { a: String, b: String ->
val valid = Valid(a)
val invalid = Invalid(b)

valid.traverseNullable<String?> { it } shouldBe valid.map<String?> { it }.sequenceNullable()
valid.traverseNullable<String?> { null } shouldBe valid.map<String?> { null }.sequenceNullable()
invalid.traverseNullable<String?> { it } shouldBe invalid.map<String?> { it }.sequenceNullable()
}
}

Expand All @@ -392,7 +413,7 @@ class ValidatedTest : UnitSpec() {
val invalid = Invalid(b)

valid.traverseEither { Right(it) } shouldBe valid.map { Right(it) }.sequenceEither()
invalid.traverseEither { Right(it) } shouldBe invalid.map { Right(it) }.sequenceEither()
invalid.traverseEither { Right(it) } shouldBe invalid.map { Right(it) }.sequenceEither()
}
}

Expand Down Expand Up @@ -438,6 +459,27 @@ class ValidatedTest : UnitSpec() {
}
}


"bitraverseNullable should wrap valid or invalid in a nullable" {
val valid = Valid("Who")
val invalid = Invalid("Nope")

valid.bitraverseNullable({ it }, { it }) shouldBe Valid("Who")
invalid.bitraverseNullable({ it }, { it }) shouldBe Invalid("Nope")
}

"bisequenceOption should yield consistent result with bitraverseOption" {
checkAll(Arb.string().orNull(), Arb.string().orNull()) { a: String?, b: String? ->
val valid: Validated<String?, String?> = Valid(a)
val invalid: Validated<String?, String?> = Invalid(b)

valid.bimap({ it }, { it }).bisequenceNullable() shouldBe
valid.bitraverseNullable({ it }, { it })
invalid.bimap({ it }, { it }).bisequenceNullable() shouldBe
invalid.bitraverseNullable({ it }, { it })
}
}

"bitraverseEither should wrap valid or invalid in an either" {
val valid = Valid("Who")
val invalid = Invalid("Nope")
Expand Down