diff --git a/arrow-core-data/src/main/kotlin/arrow/core/ListK.kt b/arrow-core-data/src/main/kotlin/arrow/core/ListK.kt index f262102a1..b8ffa6d97 100644 --- a/arrow-core-data/src/main/kotlin/arrow/core/ListK.kt +++ b/arrow-core-data/src/main/kotlin/arrow/core/ListK.kt @@ -167,14 +167,41 @@ data class ListK(private val list: List) : ListKOf, List by list } } + @Deprecated("Deprecated, use mapNotNull(f: (A) -> B?) instead", ReplaceWith("mapNotNull(f: (A) -> B?)")) fun filterMap(f: (A) -> Option): ListK = flatMap { a -> f(a).fold({ empty() }, { just(it) }) } + /** + * Returns a [ListK] containing the transformed values from the original + * [ListK] filtering out any null value. + * + * Example: + * ```kotlin:ank:playground + * import arrow.core.* + * + * //sampleStart + * val evenStrings = listOf(1, 2).k().mapNotNull { + * when (it % 2 == 0) { + * true -> it.toString() + * else -> null + * } + * } + * //sampleEnd + * + * fun main() { + * println("evenStrings = $evenStrings") + * } + * ``` + */ + fun mapNotNull(f: (A) -> B?): ListK = + flatMap { a -> f(a)?.let { just(it) } ?: empty() } + override fun hashCode(): Int = list.hashCode() /** * Align two Lists as in zip, but filling in blanks with None. */ + @Deprecated("Deprecated, use `padZipWithNull` instead", ReplaceWith("padZipWithNull(other: ListK)")) fun padZip( other: ListK ): ListK, Option>> = @@ -185,23 +212,110 @@ data class ListK(private val list: List) : ListKOf, List by list { a, b -> a.some() toT b.some() }) } + /** + * Returns a [ListK>] containing the zipped values of the two listKs + * with null for padding. + * + * Example: + * ```kotlin:ank:playground + * import arrow.core.* + * + * //sampleStart + * val padRight = listOf(1, 2).k().padZip(listOf("a").k()) // Result: ListK(Tuple2(1, "a"), Tuple2(2, null)) + * val padLeft = listOf(1).k().padZip(listOf("a", "b").k()) // Result: ListK(Tuple2(1, "a"), Tuple2(null, "b")) + * val noPadding = listOf(1, 2).k().padZip(listOf("a", "b").k()) // Result: ListK(Tuple2(1, "a"), Tuple2(2, "b")) + * //sampleEnd + * + * fun main() { + * println("padRight = $padRight") + * println("padLeft = $padLeft") + * println("noPadding = $noPadding") + * } + * ``` + */ + fun padZipWithNull( + other: ListK + ): ListK> = + alignWith(this, other) { ior -> + ior.fold( + { it toT null }, + { null toT it }, + { a, b -> a toT b }) + } + /** * Align two Lists as in zipWith, but filling in blanks with None. */ + @Deprecated("Deprecated, use `padZip(other: ListK, fa: (A?, B?) -> C)` instead", ReplaceWith("padZip(other: ListK, fa: (A?, B?) -> C)")) fun padZipWith( other: ListK, fa: (Option, Option) -> C ): ListK = padZip(other).map { fa(it.a, it.b) } + /** + * Returns a [ListK] containing the result of applying some transformation `(A?, B?) -> C` + * on a zip. + * + * Example: + * ```kotlin:ank:playground + * import arrow.core.* + * + * //sampleStart + * val padZipRight = listOf(1, 2).k().padZip(listOf("a").k()) { l, r -> l toT r }.k() // Result: ListK(Tuple2(1, "a"), Tuple2(2, null)) + * val padZipLeft = listOf(1).k().padZip(listOf("a", "b").k()) { l, r -> l toT r }.k() // Result: ListK(Tuple2(1, "a"), Tuple2(null, "b")) + * val noPadding = listOf(1, 2).k().padZip(listOf("a", "b").k()) { l, r -> l toT r }.k() // Result: ListK(Tuple2(1, "a"), Tuple2(2, "b")) + * //sampleEnd + * + * fun main() { + * println("padZipRight = $padZipRight") + * println("padZipLeft = $padZipLeft") + * println("noPadding = $noPadding") + * } + * ``` + */ + fun padZip( + other: ListK, + fa: (A?, B?) -> C + ): ListK = + padZipWithNull(other).map { fa(it.a, it.b) } + /** * Left-padded zipWith. */ + @Deprecated("Deprecated, use `leftPadZip(other: ListK, fab: (A?, B) -> C)` instead", ReplaceWith("leftPadZip(other: ListK, fab: (A?, B) -> C)")) fun lpadZipWith( other: ListK, fab: (Option, B) -> C ): ListK = - padZipWith(other) { a, b -> b.map { fab(a, it) } }.filterMap(::identity) + padZipWith(other) { a: Option, b -> b.map { fab(a, it) } }.filterMap(::identity) + + /** + * Returns a [ListK] containing the result of applying some transformation `(A?, B) -> C` + * on a zip, excluding all cases where the right value is null. + * + * Example: + * ```kotlin:ank:playground + * import arrow.core.* + * + * //sampleStart + * val left = listOf(1, 2).k().leftPadZip(listOf("a").k()) { l, r -> l toT r }.k() // Result: ListK(Tuple2(1, "a")) + * val right = listOf(1).k().leftPadZip(listOf("a", "b").k()) { l, r -> l toT r }.k() // Result: ListK(Tuple2(1, "a"), Tuple2(null, "b")) + * val both = listOf(1, 2).k().leftPadZip(listOf("a", "b").k()) { l, r -> l toT r }.k() // Result: ListK(Tuple2(1, "a"), Tuple2(2, "b")) + * //sampleEnd + * + * fun main() { + * println("left = $left") + * println("right = $right") + * println("both = $both") + * } + * ``` + */ + fun leftPadZip( + other: ListK, + fab: (A?, B) -> C + ): ListK = + padZip(other) { a: A?, b: B? -> b?.let { fab(a, it) } }.mapNotNull(::identity) /** * Left-padded zip. diff --git a/arrow-core-data/src/test/kotlin/arrow/core/ListKTest.kt b/arrow-core-data/src/test/kotlin/arrow/core/ListKTest.kt index 7e360c21e..8f7db68a1 100644 --- a/arrow-core-data/src/test/kotlin/arrow/core/ListKTest.kt +++ b/arrow-core-data/src/test/kotlin/arrow/core/ListKTest.kt @@ -3,6 +3,7 @@ package arrow.core import arrow.Kind import arrow.core.extensions.eq import arrow.core.extensions.hash +import arrow.core.extensions.list.zip.zipWith import arrow.core.extensions.listk.align.align import arrow.core.extensions.listk.applicative.applicative import arrow.core.extensions.listk.crosswalk.crosswalk @@ -23,6 +24,8 @@ import arrow.core.extensions.listk.show.show import arrow.core.extensions.listk.traverse.traverse import arrow.core.extensions.listk.unalign.unalign import arrow.core.extensions.listk.unzip.unzip +import arrow.core.extensions.listk.zip.zipWith +import arrow.core.extensions.option.eq.eq import arrow.core.extensions.show import arrow.core.test.UnitSpec import arrow.core.test.generators.genK @@ -165,6 +168,20 @@ class ListKTest : UnitSpec() { } } + "leftPadZip (with map)" { + forAll(Gen.listK(Gen.int()), Gen.listK(Gen.int())) { a, b -> + val left = a.map { it }.k() + List(max(0, b.count() - a.count())) { null }.k() + val right = b.map { it }.k() + List(max(0, a.count() - b.count())) { null }.k() + + val result = + a.leftPadZip(b) { a, b -> + a toT b + } + + result == left.zipWith(right) { l, r -> l toT r }.filter { it.b != null } + } + } + "rpadzip" { forAll(Gen.listK(Gen.int()), Gen.listK(Gen.int())) { a, b -> @@ -186,6 +203,62 @@ class ListKTest : UnitSpec() { result.map { it.a }.equalUnderTheLaw(a, ListK.eq(Int.eq())) } } + + "padZip" { + forAll(Gen.listK(Gen.int()), Gen.listK(Gen.int())) { a, b -> + val left = a.map { Some(it) }.k() + List(max(0, b.count() - a.count())) { None }.k() + val right = b.map { Some(it) }.k() + List(max(0, a.count() - b.count())) { None }.k() + + a.padZip(b) == left.zipWith(right) { l, r -> l toT r } + } + } + + "padZipWith" { + forAll(Gen.listK(Gen.int()), Gen.listK(Gen.int())) { a, b -> + val left = a.map { Some(it) }.k() + List(max(0, b.count() - a.count())) { None }.k() + val right = b.map { Some(it) }.k() + List(max(0, a.count() - b.count())) { None }.k() + a.padZipWith(b) { l, r -> Ior.fromOptions(l, r) } == left.zipWith(right) { l, r -> Ior.fromOptions(l, r) } + } + } + + "padZip (with map)" { + forAll(Gen.listK(Gen.int()), Gen.listK(Gen.int())) { a, b -> + val left = a.map { it }.k() + List(max(0, b.count() - a.count())) { null }.k() + val right = b.map { it }.k() + List(max(0, a.count() - b.count())) { null }.k() + a.padZip(b) { l, r -> Ior.fromNullables(l, r) } == left.zipWith(right) { l, r -> Ior.fromNullables(l, r) } + } + } + + "padZipWithNull" { + forAll(Gen.listK(Gen.int()), Gen.listK(Gen.int())) { a, b -> + val left = a.map { it }.k() + List(max(0, b.count() - a.count())) { null }.k() + val right = b.map { it }.k() + List(max(0, a.count() - b.count())) { null }.k() + + a.padZipWithNull(b) == left.zipWith(right) { l, r -> l toT r } + } + } + + "filterMap() should map list and filter out None values" { + forAll(Gen.listK(Gen.int())) { listk -> + listk.filterMap { + when (it % 2 == 0) { + true -> it.toString().toOption() + else -> None + } + } == listk.toList().filter { it % 2 == 0 }.map { it.toString() }.k() + } + } + + "mapNotNull() should map list and filter out null values" { + forAll(Gen.listK(Gen.int())) { listk -> + listk.mapNotNull { + when (it % 2 == 0) { + true -> it.toString() + else -> null + } + } == listk.toList().filter { it % 2 == 0 }.map { it.toString() }.k() + } + } } private fun bijection(from: Kind, Int>>): ListK>> =