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

add some Iterable tests (#2894) #3040

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -1272,36 +1272,75 @@ public fun <A> Iterable<A>.fold(MA: Monoid<A>): A =
public fun <A, B> Iterable<A>.foldMap(MB: Monoid<B>, f: (A) -> B): B =
fold(MB.empty()) { acc, a -> MB.run { acc.combine(f(a)) } }

/**
* Applies function [f] to each element
* and returns a list for the applied result Iterable<B>.
Comment on lines +1276 to +1277
Copy link
Contributor Author

@jsoizo jsoizo Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this explanation is adequate and would be happy to receive a revised proposal.
The same goes for crosswalkMap and crosswalkNull.

*
* ```kotlin
* import arrow.core.crosswalk
* import io.kotest.matchers.shouldBe
*
* fun test() {
* val ints = listOf(1, 2)
* val res = ints.crosswalk { i -> listOf("a${i}", "b${i}", "c${i}") }
* res shouldBe listOf(listOf("a1", "a2"), listOf("b1", "b2"), listOf("c1", "c2"))
* }
* ```
*/
public fun <A, B> Iterable<A>.crosswalk(f: (A) -> Iterable<B>): List<List<B>> =
fold(emptyList()) { bs, a ->
f(a).align(bs) { ior ->
ior.fold(
{ listOf(it) },
::identity,
{ l, r -> listOf(l) + r }
{ l, r -> r + l }
)
}
}

/**
* Applies function [f] to each element
* and returns the concatenated Map of the applied result Map<K, V>.
*
* ```kotlin
* import arrow.core.crosswalk
* import io.kotest.matchers.shouldBe
*
* fun test() {
* val ints = listOf(1, 2)
* val res = ints.crosswalkMap { i -> mapOf("a" to i, "b" to i, "c" to i) }
* res shouldBe listOf("a", "b", "c").map { a -> a to ints }.toMap()
* }
* ```
*/
public fun <A, K, V> Iterable<A>.crosswalkMap(f: (A) -> Map<K, V>): Map<K, List<V>> =
fold(emptyMap()) { bs, a ->
f(a).align(bs) { (_, ior) ->
ior.fold(
{ listOf(it) },
::identity,
{ l, r -> listOf(l) + r }
{ l, r -> r + l }
)
}
}

/**
* Applies function [f] to each element
* and returns the result Iterable<B> without null.
*
* ```kotlin
* import arrow.core.crosswalk
* import io.kotest.matchers.shouldBe
*
* fun test() {
* val ints = listOf(1, 2)
* val res = ints.crosswalkNull { i -> if (i % 2 == 0) "x${i}" else null }
* res shouldBe listOf("x2")
* }
* ```
*/
public fun <A, B> Iterable<A>.crosswalkNull(f: (A) -> B?): List<B>? =
Copy link
Contributor Author

@jsoizo jsoizo Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you pass emptyList() as the first argument of fold, the results of Ior.fromNullables and croswalkNull will also not be null, since bs is always non-null. If you expect croswalkNull to be null if Iterable<A> is empty or all results of applying the function f are null, it is better to pass null as the first argument of fold.
Furthermore, it appears that if we modify it as described, crosswalkNull will no longer return an empty list, so it would be a good idea to set the return type to NonEmptyList<B>? ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, this function should:

  • return null if (and only if) f(it) returns null for some component,
  • return emptyList() if the Iterable is empty
  • return non-empty for a non-empty list where evey application of f returns non-null.

Does this make sense? Does this coincide with the behavior here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@serras Thank you for your reply.

Does this make sense?

I have the same understanding as you have written.

Does this coincide with the behavior here?

However, the behavior seems not to coincides with the understandings.
The first and third case of following tests fails.

class CrosswalkNullTest : FunSpec({
    test("return null if f(it) returns null") {
        val list = listOf(1, 2, 3)
        val result = list.crosswalkNull { null }
        // This test fails because result is emptyList()
        result shouldBe null
    }

    test("return emptyList() if Iterable is empty") {
        val list = emptyList<Int>()
        val result = list.crosswalkNull { it.toString() }
        // This test passes
        result shouldBe emptyList()
    }

    test("return non-empty if Iterable is non-empty and f(it) returns not-null") {
        val list = listOf(1, 2, 3)
        val result = list.crosswalkNull { it.toString() }
        // This test fails because result is listOf("3", "2", "1")
        result shouldBe listOf("1", "2", "3")
    }
})

fold<A, List<B>?>(emptyList()) { bs, a ->
Ior.fromNullables(f(a), bs)?.fold(
{ listOf(it) },
::identity,
{ l, r -> listOf(l) + r }
)
}
mapNotNull(f).takeIf { it.isNotEmpty() || !this.any() }

@Deprecated("Not being used anymore. Will be removed from the binary in 2.0.0")
@PublishedApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import io.kotest.property.arbitrary.list
import io.kotest.property.arbitrary.orNull
import io.kotest.property.arbitrary.pair
import io.kotest.property.arbitrary.string
import io.kotest.property.arbitrary.char
import io.kotest.property.checkAll
import kotlin.math.max
import kotlin.math.min
Expand Down Expand Up @@ -555,6 +556,26 @@ class IterableTest : StringSpec({
}
}

"flatten" {
checkAll(Arb.pair(Arb.list(Arb.int()), Arb.list(Arb.int()))) { (a, b) ->
listOf(a, b).flatten() shouldBe a + b
}
}

"widen(Iterable)" {
checkAll(Arb.list(Arb.string())) { orig: Iterable<String> ->
val result: Iterable<CharSequence> = orig.widen()
result shouldContainExactly orig
}
}

"widen(List)" {
checkAll(Arb.list(Arb.string())) { orig: List<String> ->
val result: List<CharSequence> = orig.widen()
result shouldContainExactly orig
}
}

"unzip is the inverse of zip" {
checkAll(Arb.list(Arb.int())) { xs ->

Expand Down Expand Up @@ -630,4 +651,54 @@ class IterableTest : StringSpec({
}
}
}

"compareTo returns 0 if other has same elements" {
checkAll(Arb.list(Arb.int())) { ints ->
ints.compareTo(ints) shouldBe 0
}
}

"compareTo returns -1 if other have greater element at the same position"{
listOf(1,2,3).compareTo(listOf(1,4,3)) shouldBe -1
}

"compareTo returns 1 if other have smaller element at the same position"{
listOf(1,2,3).compareTo(listOf(1,1,3)) shouldBe 1
}

"crosswalk" {
checkAll(Arb.pair(Arb.list(Arb.int()), Arb.list(Arb.char()))) { (ints, chars) ->
val res = ints.crosswalk { i -> chars.map { c -> "${c}${i}" } }
val expected =
if (ints.isNotEmpty()) chars.map { c -> ints.map { i -> "${c}${i}" } }
else emptyList()
res shouldBe expected
}
}

"crosswalkMap" {
checkAll(Arb.pair(Arb.list(Arb.int()), Arb.list(Arb.char()))) { (ints, chars) ->
val res = ints.crosswalkMap { i -> chars.map { c -> c to i }.toMap() }
val expected =
if (ints.isNotEmpty()) chars.map { c -> c to ints }.toMap()
else emptyMap()
res shouldBe expected
}
}

"crosswalkNull" {
checkAll(Arb.list(Arb.int(), 2 .. 20)) { ints ->
val res = ints.crosswalkNull { i -> if (i % 2 == 0) "x${i}" else null }
val expected = ints.mapNotNull { i -> if (i % 2 == 0 ) "x${i}" else null }
res shouldBe expected
}
}

"crosswalkNull, result is null" {
checkAll(Arb.list(Arb.int(), 2 .. 20)) { ints ->
val res = ints.crosswalkNull { i -> null }
res shouldBe null
}
}

})
Loading