From ba94005ee1cbf404948afd122e7d3c101406d866 Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Thu, 9 Mar 2023 09:55:01 +0100 Subject: [PATCH 01/13] improve map coverage --- .../src/commonMain/kotlin/arrow/core/map.kt | 2 +- .../commonTest/kotlin/arrow/core/MapKTest.kt | 433 ++++++++++++++++-- 2 files changed, 404 insertions(+), 31 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt index 1265ec598c6..a0bd70cd89c 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt @@ -354,7 +354,7 @@ public inline fun Map.filterIsInstance(): Map = */ public fun Map.align(b: Map): Map> = (keys + b.keys).mapNotNull { key -> - Ior.fromNullables(this[key], b[key])?.let { key to it } + Ior.fromNullables(this[key], b[key])!!.let { key to it } }.toMap() /** diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt index adfac96fe4f..61efb07e183 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt @@ -1,20 +1,35 @@ package arrow.core import arrow.core.test.intSmall +import arrow.core.test.ior import arrow.core.test.laws.MonoidLaws import arrow.core.test.longSmall import arrow.core.test.nonEmptyList +import arrow.core.test.option import arrow.core.test.testLaws import arrow.typeclasses.Monoid import arrow.typeclasses.Semigroup import io.kotest.core.spec.style.StringSpec +import io.kotest.inspectors.forAll +import io.kotest.inspectors.forAllValues +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.maps.shouldContain +import io.kotest.matchers.maps.shouldContainKey +import io.kotest.matchers.maps.shouldNotContainKey +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.property.Arb import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.choice import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.list import io.kotest.property.arbitrary.long import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.pair import io.kotest.property.arbitrary.string import io.kotest.property.checkAll @@ -65,16 +80,23 @@ class MapKTest : StringSpec({ } "traverseOption short-circuits" { - checkAll(Arb.nonEmptyList(Arb.int())) { ints -> - val acc = mutableListOf() - val evens = ints.traverse { - (it % 2 == 0).maybe { - acc.add(it) - it + checkAll(Arb.map(Arb.int(), Arb.int())) { ints -> + var shortCircuited = 0 + val result = ints.traverse { + if (it % 2 == 0) { + Some(it) + } else { + shortCircuited++ + None } } - acc shouldBe ints.takeWhile { it % 2 == 0 } - evens.fold({ Unit }) { it shouldBe ints } + shortCircuited.shouldBeIn(0, 1) + + if (shortCircuited == 0) { + result.isSome().shouldBeTrue() + } else if (shortCircuited == 1) { + result.isNone().shouldBeTrue() + } } } @@ -112,13 +134,13 @@ class MapKTest : StringSpec({ "can align maps" { // aligned keySet is union of a's and b's keys - checkAll(Arb.map(Arb.long(), Arb.boolean()), Arb.map(Arb.long(), Arb.boolean())) { a, b -> + checkAll(Arb.map(KEY_ARB, Arb.boolean()), Arb.map(KEY_ARB, Arb.boolean())) { a, b -> val aligned = a.align(b) aligned.size shouldBe (a.keys + b.keys).size } // aligned map contains Both for all entries existing in a and b - checkAll(Arb.map(Arb.long(), Arb.boolean()), Arb.map(Arb.long(), Arb.boolean())) { a, b -> + checkAll(Arb.map(KEY_ARB, Arb.boolean()), Arb.map(KEY_ARB, Arb.boolean())) { a, b -> val aligned = a.align(b) a.keys.intersect(b.keys).forEach { aligned[it]?.isBoth shouldBe true @@ -126,7 +148,7 @@ class MapKTest : StringSpec({ } // aligned map contains Left for all entries existing only in a - checkAll(Arb.map(Arb.long(), Arb.boolean()), Arb.map(Arb.long(), Arb.boolean())) { a, b -> + checkAll(Arb.map(KEY_ARB, Arb.boolean()), Arb.map(KEY_ARB, Arb.boolean())) { a, b -> val aligned = a.align(b) (a.keys - b.keys).forEach { key -> aligned[key]?.isLeft shouldBe true @@ -134,7 +156,7 @@ class MapKTest : StringSpec({ } // aligned map contains Right for all entries existing only in b - checkAll(Arb.map(Arb.long(), Arb.boolean()), Arb.map(Arb.long(), Arb.boolean())) { a, b -> + checkAll(Arb.map(KEY_ARB, Arb.boolean()), Arb.map(KEY_ARB, Arb.boolean())) { a, b -> val aligned = a.align(b) (b.keys - a.keys).forEach { key -> aligned[key]?.isRight shouldBe true @@ -142,10 +164,359 @@ class MapKTest : StringSpec({ } } + "zip is idempotent" { + checkAll( + Arb.map(KEY_ARB, Arb.intSmall())) { + a -> + a.zip(a) shouldBe a.mapValues { it.value to it.value } + } + } + + "zip is commutative" { + checkAll( + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()) + ) { + a, + b -> + + a.zip(b).mapValues { it.value.second to it.value.first } shouldBe b.zip(a) + } + } + + "zip is associative" { + checkAll( + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()) + ) { + a,b,c -> + + fun Pair, C>.assoc(): Pair> = + this.first.first to (this.first.second to this.second) + + a.zip(b.zip(c)) shouldBe (a.zip(b)).zip(c).mapValues { it.value.assoc() } + } + } + + "zip with" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()) + ) { a, b -> + val fn = { k: Int, l: String, r: String -> "$k $l $r" } + a.zip(b, fn) shouldBe a.zip(b).mapValues { fn(it.key, it.value.first, it.value.second) } + } + } + + "zip functoriality" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string())) { + a,b -> + + val f = { e: String -> "f${e}" } + val g = { e: String -> "g${e}" } + fun Pair.bimap(f: (A) -> B, g: (C) -> D) = Pair(f(first), g(second)) + + val l = a.mapValues{ f(it.value)}.zip(b.mapValues{g(it.value)}) + val r = a.zip(b).mapValues { it.value.bimap(f,g)} + + l shouldBe r + } + } + + "zippyness1" { + checkAll( + Arb.map(Arb.intSmall(), Arb.string())) { + xs -> + xs.zip(xs).mapValues { it.value.first } shouldBe xs + } + } + + "zippyness2" { + checkAll( + Arb.map(Arb.intSmall(), Arb.string())) { + xs -> + xs.zip(xs).mapValues { it.value.second } shouldBe xs + } + } + + "zippyness3" { + checkAll( + Arb.map(Arb.intSmall(), Arb.pair(Arb.string(), Arb.int()))) { + xs -> + xs.mapValues { it.value.first }.zip(xs.mapValues { it.value.second }) shouldBe xs + } + } + + "distributivity1" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()) + ) {x,y,z -> + + fun Pair, Ior>.undistrThesePair(): Ior, C> = + when (val l = this.first) { + is Ior.Left -> { + when (val r = this.second) { + is Ior.Left -> Ior.Left(l.value to r.value) + is Ior.Both -> Ior.Both(l.value to r.leftValue, r.rightValue) + is Ior.Right -> Ior.Right(r.value) + } + } + is Ior.Both -> when (val r = this.second) { + is Ior.Left -> Ior.Both(l.leftValue to r.value, l.rightValue) + is Ior.Both -> Ior.Both(l.leftValue to r.leftValue, l.rightValue) + is Ior.Right -> Ior.Right(l.rightValue) + } + is Ior.Right -> Ior.Right(l.value) + } + + val ls = x.zip(y).align(z) + val rs = x.align(z).zip(y.align(z)).mapValues { it.value.undistrThesePair() } + + ls shouldBe rs + } + } + + "distributivity2" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()) + ) {x,y,z -> + + fun Pair, C>.distrPairThese(): Ior, Pair> = + when (val l = this.first) { + is Ior.Left -> Ior.Left(l.value to this.second) + is Ior.Right -> Ior.Right(l.value to this.second) + is Ior.Both -> Ior.Both(l.leftValue to this.second, l.rightValue to this.second) + } + + val ls = x.align(y).zip(z).mapValues { it.value.distrPairThese() } + val rs = x.zip(z).align(y.zip(z)) + + ls shouldBe rs + } + } + + "distributivity3" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()) + ) {x,y,z -> + + fun Ior, Pair>.undistrPairThese(): Pair, C> = + when (val e = this) { + is Ior.Left -> Ior.Left(e.value.first) to e.value.second + is Ior.Both -> Ior.Both(e.leftValue.first, e.rightValue.first) to e.leftValue.second + is Ior.Right -> Ior.Right(e.value.first) to e.value.second + } + + val ls = x.align(y).zip(z) + val rs = x.zip(z).align(y.zip(z)).mapValues { it.value.undistrPairThese() } + + ls shouldBe rs + } + } + + "unzip is the inverse of zip" { + checkAll( + Arb.map(Arb.intSmall(), Arb.string()) + ) { xs -> + val ls = xs.zip(xs).unzip() + val rs = xs to xs + + ls shouldBe rs + } + } + + "zip is the inverse of unzip" { + checkAll( + Arb.map(Arb.intSmall(), Arb.pair(Arb.string(), Arb.int())) + ) { xs -> + val (a,b) = xs.unzip() + a.zip(b) shouldBe xs + } + } + + "unzip with" { + checkAll( + Arb.map(Arb.intSmall(), Arb.pair(Arb.string(), Arb.int())) + ) { xs -> + xs.unzip { it.value.first to it.value.second } shouldBe xs.unzip() + } + } + + "unalign with" { + checkAll( + Arb.map(Arb.intSmall(), Arb.ior(Arb.string(), Arb.int())) + ) { xs -> + xs.unalign { it.value } shouldBe xs.unalign() + } + } + + "getOrNone" { + checkAll( + Arb.map(Arb.int(0 .. 1000), Arb.string()) + ) { xs -> + val (found, notFound) = (0 .. 1000).partition { xs.containsKey(it) } + + found.forAll { + xs.getOrNone(it) + .shouldBeInstanceOf>() + .value.shouldBe(xs[it]) + } + + notFound.forAll { + xs.getOrNone(it) + .shouldBeInstanceOf() + } + } + } + + "unalign is the inverse of align" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()) + ) { a,b -> + a.align(b).unalign() shouldBe (a to b) + } + } + + "align is the inverse of unalign" { + checkAll( + Arb.map(Arb.intSmall(), Arb.ior(Arb.int(), Arb.string())) + ) { xs -> + val (a,b) = xs.unalign() + + a.align(b) shouldBe xs + } + } + + "padZip" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()) + ) { a, b -> + val x = a.padZip(b) + + a.forAll { + val value: Pair = x[it.key].shouldNotBeNull() + + value.first shouldBe it.value + } + + b.forAll { + val value: Pair = x[it.key].shouldNotBeNull() + + value.second shouldBe it.value + } + } + } + + "padZip with" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()) + ) { a, b -> + a.padZip(b) { a,b,c -> "$a $b $c"} shouldBe a.padZip(b).mapValues { "${it.key} ${it.value.first} ${it.value.second}"} + } + } + + "salign" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()) + ) { + a,b -> + a.salign(Semigroup.string(), b) shouldBe a.align(b) {it.value.fold(::identity, ::identity) { a, b -> a + b } } + } + } + + "void" { + checkAll( + Arb.map(Arb.intSmall(), Arb.intSmall()) + ) { a -> + val result = a.void() + + result.keys shouldBe a.keys + result.forAllValues { it shouldBe Unit } + } + } + + "filterMap" { + checkAll( + Arb.map(Arb.int(), Arb.boolean()) + ) { xs -> + val rs = xs.filterMap { if(it) true else null } + + xs.forAll { + if (it.value) + rs shouldContainKey it.key + else + rs shouldNotContainKey it.key + } + } + } + + "filterOption" { + checkAll( + Arb.map(Arb.int(), Arb.option(Arb.string())) + ) { xs -> + val rs = xs.filterOption() + + xs.forAll { + val value = it.value + if (value is Some) + rs shouldContain (it.key to value.value) + else + rs shouldNotContainKey it.key + } + } + } + + "filterInstance" { + checkAll( + Arb.map(Arb.int(), Arb.choice(Arb.int(), Arb.string())) + ) { xs -> + val a = xs.filterIsInstance() + val b = xs.filterIsInstance() + + (a + b) shouldBe xs + } + } + + + "ensure that Arb used for map keys produces a small enough set of distinct values" { + + /* + when zipping/aligning maps we will execute different code paths depending on if a given key is present in both maps or not. + therefor we need to make sure to use an Arb here that produces a small enough set of distint values. + this test is to ensure that the arb in use should cause at least 50 iterations with at least 10 keys in both maps + */ + + val result = mutableListOf() + + val arb = KEY_ARB + checkAll( + Arb.map(arb, Arb.intSmall()), + Arb.map(arb, Arb.intSmall()) + ) { a, b -> + result.add((a.keys.intersect(b.keys)).size) + } + + result.count { it > 10 } shouldBeGreaterThan 50 + } + + "zip2" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), Arb.intSmall()) + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()) ) { a, b -> val result = a.zip(b) { _, aa, bb -> Pair(aa, bb) } val expected = a.filter { (k, _) -> b.containsKey(k) } @@ -158,8 +529,8 @@ class MapKTest : StringSpec({ "zip3" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), Arb.intSmall()) + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()) ) { a, b -> val result = a.zip(b, b) { _, aa, bb, cc -> Triple(aa, bb, cc) } @@ -173,8 +544,8 @@ class MapKTest : StringSpec({ "zip4" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()), ) { a, b -> val result = a.zip(b, b, b) { _, aa, bb, cc, dd -> Tuple4(aa, bb, cc, dd) } @@ -188,8 +559,8 @@ class MapKTest : StringSpec({ "zip5" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()), ) { a, b -> val result = a.zip(b, b, b, b) { _, aa, bb, cc, dd, ee -> Tuple5(aa, bb, cc, dd, ee) } @@ -203,8 +574,8 @@ class MapKTest : StringSpec({ "zip6" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()), ) { a, b -> val result = a.zip(b, b, b, b, b) { _, aa, bb, cc, dd, ee, ff -> Tuple6(aa, bb, cc, dd, ee, ff) } @@ -218,8 +589,8 @@ class MapKTest : StringSpec({ "zip7" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), Arb.intSmall()) + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()) ) { a, b -> val result = a.zip(b, b, b, b, b, b) { _, aa, bb, cc, dd, ee, ff, gg -> Tuple7(aa, bb, cc, dd, ee, ff, gg) } @@ -233,8 +604,8 @@ class MapKTest : StringSpec({ "zip8" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), Arb.intSmall()) + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()) ) { a, b -> val result = a.zip(b, b, b, b, b, b, b) { _, aa, bb, cc, dd, ee, ff, gg, hh -> Tuple8(aa, bb, cc, dd, ee, ff, gg, hh) } @@ -249,8 +620,8 @@ class MapKTest : StringSpec({ "zip9" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), Arb.intSmall()) + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()) ) { a, b -> val result = a.zip(b, b, b, b, b, b, b, b) { _, aa, bb, cc, dd, ee, ff, gg, hh, ii -> Tuple9( @@ -276,8 +647,8 @@ class MapKTest : StringSpec({ "zip10" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), Arb.intSmall()) + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()) ) { a, b -> val result = a.zip(b, b, b, b, b, b, b, b, b) { _, aa, bb, cc, dd, ee, ff, gg, hh, ii, jj -> Tuple10( @@ -316,3 +687,5 @@ class MapKTest : StringSpec({ } }) + +private val KEY_ARB = Arb.int(0 .. 250) From f3892c25514d78df3222f18a2351000c60fc1822 Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Thu, 9 Mar 2023 20:50:47 +0100 Subject: [PATCH 02/13] mapOrAccumulate tests --- .../commonTest/kotlin/arrow/core/MapKTest.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt index 61efb07e183..c1b2c7eb6e0 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt @@ -19,6 +19,7 @@ import io.kotest.matchers.ints.shouldBeGreaterThan import io.kotest.matchers.maps.shouldContain import io.kotest.matchers.maps.shouldContainKey import io.kotest.matchers.maps.shouldNotContainKey +import io.kotest.matchers.maps.shouldNotHaveValues import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.property.Arb import io.kotest.matchers.shouldBe @@ -686,6 +687,43 @@ class MapKTest : StringSpec({ } } + "mapOrAccumulate can map" { + checkAll( + Arb.map(Arb.int(), Arb.int()) + ) { xs -> + + val result: Either, Map> = xs.mapOrAccumulate { + it.value.toString() + } + + result.shouldBeInstanceOf>>() + xs.forAll { + result.value.shouldContain(it.key to it.value.toString()) + } + } + } + + "mapOrAccumulate accumulates errors" { + checkAll( + Arb.map(Arb.int(), Arb.boolean()) + ) { xs -> + + val result: Either, Map> = xs.mapOrAccumulate { + if (it.value) { + raise(it.key) + } else { + "" + } + } + + result.fold({ nel -> + nel.all shouldBe xs.filter { it.value }.keys.toSet() + }, { + xs.shouldNotHaveValues(true) + }) + } + } + }) private val KEY_ARB = Arb.int(0 .. 250) From 5438703afaee056c6f4cafc898b697bcd92fc7ac Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Sat, 11 Mar 2023 21:10:59 +0100 Subject: [PATCH 03/13] wip --- .../src/commonTest/kotlin/arrow/core/MapKTest.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt index c1b2c7eb6e0..8cd1eb2733e 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt @@ -4,7 +4,6 @@ import arrow.core.test.intSmall import arrow.core.test.ior import arrow.core.test.laws.MonoidLaws import arrow.core.test.longSmall -import arrow.core.test.nonEmptyList import arrow.core.test.option import arrow.core.test.testLaws import arrow.typeclasses.Monoid @@ -12,10 +11,10 @@ import arrow.typeclasses.Semigroup import io.kotest.core.spec.style.StringSpec import io.kotest.inspectors.forAll import io.kotest.inspectors.forAllValues -import io.kotest.matchers.booleans.shouldBeFalse import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.collections.shouldBeIn import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.maps.shouldBeEmpty import io.kotest.matchers.maps.shouldContain import io.kotest.matchers.maps.shouldContainKey import io.kotest.matchers.maps.shouldNotContainKey @@ -28,7 +27,6 @@ import io.kotest.property.arbitrary.boolean import io.kotest.property.arbitrary.choice import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.list -import io.kotest.property.arbitrary.long import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.pair import io.kotest.property.arbitrary.string @@ -687,6 +685,15 @@ class MapKTest : StringSpec({ } } + "mapOrAccumulate of empty should be empty" { + val result: Either, Map> = emptyMap().mapOrAccumulate { + it.value.toString() + } + + result.shouldBeInstanceOf>>() + .value.shouldBeEmpty() + } + "mapOrAccumulate can map" { checkAll( Arb.map(Arb.int(), Arb.int()) From 2c00de3e32b244ad42c43faff4a18ec86b0b128e Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Tue, 14 Mar 2023 11:36:55 +0100 Subject: [PATCH 04/13] enhance function Arbs --- .../commonTest/kotlin/arrow/core/MapKTest.kt | 132 ++++++++++++++++-- .../kotlin/arrow/core/test/Generators.kt | 12 +- .../kotlin/arrow/core/test/GeneratorsTest.kt | 57 ++++++++ 3 files changed, 186 insertions(+), 15 deletions(-) create mode 100644 arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt index 8cd1eb2733e..6ba39f09ceb 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt @@ -1,5 +1,7 @@ package arrow.core +import arrow.core.test.functionABCToD +import arrow.core.test.functionAToB import arrow.core.test.intSmall import arrow.core.test.ior import arrow.core.test.laws.MonoidLaws @@ -20,9 +22,9 @@ import io.kotest.matchers.maps.shouldContainKey import io.kotest.matchers.maps.shouldNotContainKey import io.kotest.matchers.maps.shouldNotHaveValues import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.property.Arb import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf +import io.kotest.property.Arb import io.kotest.property.arbitrary.boolean import io.kotest.property.arbitrary.choice import io.kotest.property.arbitrary.int @@ -171,6 +173,14 @@ class MapKTest : StringSpec({ } } + "align is idempotent" { + checkAll( + Arb.map(KEY_ARB, Arb.intSmall())) { + a -> + a.align(a) shouldBe a.mapValues { Ior.Both(it.value, it.value) } + } + } + "zip is commutative" { checkAll( Arb.map(KEY_ARB, Arb.intSmall()), @@ -179,7 +189,19 @@ class MapKTest : StringSpec({ a, b -> - a.zip(b).mapValues { it.value.second to it.value.first } shouldBe b.zip(a) + a.zip(b) shouldBe b.zip(a).mapValues { it.value.second to it.value.first } + } + } + + "align is commutative" { + checkAll( + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()) + ) { + a, + b -> + + a.align(b) shouldBe b.align(a).mapValues { it.value.swap() } } } @@ -198,24 +220,62 @@ class MapKTest : StringSpec({ } } + "align is associative" { + checkAll( + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()), + Arb.map(KEY_ARB, Arb.intSmall()) + ) { + a,b,c -> + + fun Ior, C>.assoc(): Ior> = + when (this) { + is Ior.Left -> when (val inner = this.value) { + is Ior.Left -> Ior.Left(inner.value) + is Ior.Right -> Ior.Right(Ior.Left(inner.value)) + is Ior.Both -> Ior.Both(inner.leftValue, Ior.Left(inner.rightValue)) + } + is Ior.Right -> Ior.Right(Ior.Right(this.value)) + is Ior.Both -> when (val inner = this.leftValue) { + is Ior.Left -> Ior.Both(inner.value, Ior.Right(this.rightValue)) + is Ior.Right -> Ior.Right(Ior.Both(inner.value, this.rightValue)) + is Ior.Both -> Ior.Both(inner.leftValue, Ior.Both(inner.rightValue, this.rightValue)) + } + } + + a.align(b.align(c)) shouldBe (a.align(b)).align(c).mapValues { it.value.assoc() } + } + } + "zip with" { checkAll( Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()) - ) { a, b -> - val fn = { k: Int, l: String, r: String -> "$k $l $r" } + Arb.map(KEY_ARB, Arb.string()), + Arb.functionABCToD(Arb.string()) + ) { a, b, fn -> a.zip(b, fn) shouldBe a.zip(b).mapValues { fn(it.key, it.value.first, it.value.second) } } } + "align with" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()), + Arb.functionAToB>, String>(Arb.string()) + ) { a, b, fn -> + a.align(b, fn) shouldBe a.align(b).mapValues { fn(it) } + } + } + "zip functoriality" { checkAll( Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string())) { - a,b -> + Arb.map(KEY_ARB, Arb.string()), + Arb.functionAToB(Arb.string()), + Arb.functionAToB(Arb.string()) + ) { + a,b,f,g -> - val f = { e: String -> "f${e}" } - val g = { e: String -> "g${e}" } fun Pair.bimap(f: (A) -> B, g: (C) -> D) = Pair(f(first), g(second)) val l = a.mapValues{ f(it.value)}.zip(b.mapValues{g(it.value)}) @@ -225,6 +285,50 @@ class MapKTest : StringSpec({ } } + "align functoriality" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()), + Arb.functionAToB(Arb.string()), + Arb.functionAToB(Arb.string()) + ) { + a,b,f,g -> + + val l = a.mapValues{ f(it.value)}.align(b.mapValues{g(it.value)}) + val r = a.align(b).mapValues { it.value.bimap(f,g)} + + l shouldBe r + } + } + + "alignedness" { + checkAll( + Arb.map(KEY_ARB, Arb.string()), + Arb.map(KEY_ARB, Arb.string()) + ) { + a ,b-> + + fun toList(es: Map): List = + es.fold(emptyList()) { + acc, e -> acc + e.value + } + + val left = toList(a) + + fun Ior.toLeftOption() = + fold({it}, {null}, {a,_ -> a}) + + // toListOf (folded . here) (align x y) + val middle = toList(a.align(b).mapValues { it.value.toLeftOption() }).filterNotNull() + + // mapMaybe justHere (toList (align x y)) + val right = toList(a.align(b)).mapNotNull { it.toLeftOption() } + + left shouldBe right + left shouldBe middle + } + } + "zippyness1" { checkAll( Arb.map(Arb.intSmall(), Arb.string())) { @@ -420,9 +524,10 @@ class MapKTest : StringSpec({ "padZip with" { checkAll( Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()) - ) { a, b -> - a.padZip(b) { a,b,c -> "$a $b $c"} shouldBe a.padZip(b).mapValues { "${it.key} ${it.value.first} ${it.value.second}"} + Arb.map(KEY_ARB, Arb.string()), + Arb.functionABCToD(Arb.string()) + ) { a, b, fn -> + a.padZip(b, fn) shouldBe a.padZip(b).mapValues { fn(it.key, it.value.first, it.value.second) } } } @@ -493,7 +598,8 @@ class MapKTest : StringSpec({ "ensure that Arb used for map keys produces a small enough set of distinct values" { /* - when zipping/aligning maps we will execute different code paths depending on if a given key is present in both maps or not. + when zipping/aligning maps we will execute different code paths depending if a given key is present in both maps or not. + when using types with lots of values like String/Int etc. this is most likely not the case. In effect the tests will not cover all code branches. therefor we need to make sure to use an Arb here that produces a small enough set of distint values. this test is to ensure that the arb in use should cause at least 50 iterations with at least 10 keys in both maps */ diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt index 226ef102150..40a56b1f727 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt @@ -9,10 +9,12 @@ import arrow.core.NonEmptySet import arrow.core.Option import arrow.core.Validated import arrow.core.left +import arrow.core.memoize import arrow.core.right import arrow.core.toNonEmptySetOrNull import arrow.core.toOption import io.kotest.property.Arb +import io.kotest.property.arbitrary.arbitrary import io.kotest.property.arbitrary.bind import io.kotest.property.arbitrary.boolean import io.kotest.property.arbitrary.choice @@ -22,6 +24,7 @@ import io.kotest.property.arbitrary.list import io.kotest.property.arbitrary.set import io.kotest.property.arbitrary.long import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.next import io.kotest.property.arbitrary.of import io.kotest.property.arbitrary.orNull import io.kotest.property.arbitrary.string @@ -46,8 +49,13 @@ fun Arb.Companion.nonEmptySet(arb: Arb, range: IntRange = 0 .. 100): Arb< fun Arb.Companion.sequence(arb: Arb, range: IntRange = 0 .. 100): Arb> = Arb.list(arb, range).map { it.asSequence() } -fun Arb.Companion.functionAToB(arb: Arb): Arb<(A) -> B> = - arb.map { b: B -> { _: A -> b } } +fun Arb.Companion.functionAToB(arbB: Arb): Arb<(A) -> B> = arbitrary {random -> + { _: A -> arbB.next(random) }.memoize() +} + +fun Arb.Companion.functionABCToD(arb: Arb): Arb<(A, B, C) -> D> = arbitrary {random -> + ({ _: A, _:B, _:C -> arb.next(random)}.memoize()) +} fun Arb.Companion.throwable(): Arb = Arb.of(listOf(RuntimeException(), NoSuchElementException(), IllegalArgumentException())) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt new file mode 100644 index 00000000000..dc6d40123f1 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt @@ -0,0 +1,57 @@ +package arrow.core.test + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.inspectors.forAtLeastOne +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.string +import io.kotest.property.arbitrary.take +import io.kotest.property.assume +import io.kotest.property.checkAll + +class GeneratorsTest : FreeSpec( { + + "functionAToB" - { + + "should return same result when invoked multiple times" { + checkAll(Arb.string(), Arb.functionAToB(Arb.int())) { a, fn -> + fn(a) shouldBe fn(a) + } + } + + "should return some different values" { + Arb.functionAToB(Arb.int()).take(100) + .forAtLeastOne { fn -> + checkAll(100, Arb.string(), Arb.string()) { + a,b -> + assume(a != b) + fn(a) shouldNotBe fn(b) + } + } + } + } + + "functionABCToD" - { + + "should return same result when invoked multiple times" { + checkAll(Arb.string(), Arb.string(), Arb.string(), Arb.functionABCToD(Arb.int())) { a, b, c, fn -> + fn(a,b,c) shouldBe fn(a,b,c) + } + } + + "should return some different values" { + Arb.functionABCToD(Arb.int()).take(100) + .forAtLeastOne { fn -> + checkAll(100, Arb.string(), Arb.string(), Arb.string()) { + a,b,c -> + assume(a != c) + fn(a,b,c) shouldNotBe fn(c,b,a) + } + } + } + + } + +}) From 13e6aafb8b918ec59163fbb07925a45dd6d44b09 Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Wed, 15 Mar 2023 09:49:06 +0100 Subject: [PATCH 05/13] format --- .../src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt index dc6d40123f1..d7f83af65ac 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt @@ -14,7 +14,6 @@ import io.kotest.property.checkAll class GeneratorsTest : FreeSpec( { "functionAToB" - { - "should return same result when invoked multiple times" { checkAll(Arb.string(), Arb.functionAToB(Arb.int())) { a, fn -> fn(a) shouldBe fn(a) @@ -34,7 +33,6 @@ class GeneratorsTest : FreeSpec( { } "functionABCToD" - { - "should return same result when invoked multiple times" { checkAll(Arb.string(), Arb.string(), Arb.string(), Arb.functionABCToD(Arb.int())) { a, b, c, fn -> fn(a,b,c) shouldBe fn(a,b,c) @@ -51,7 +49,6 @@ class GeneratorsTest : FreeSpec( { } } } - } }) From 87db31ade8a0b5264cbe45986a9e5349edac3b49 Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Wed, 15 Mar 2023 12:11:23 +0100 Subject: [PATCH 06/13] revert functionAToB behaviour --- .../kotlin/arrow/core/test/Generators.kt | 7 ++-- .../kotlin/arrow/core/test/GeneratorsTest.kt | 39 ++++++------------- 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt index 40a56b1f727..759077cc529 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt @@ -49,11 +49,10 @@ fun Arb.Companion.nonEmptySet(arb: Arb, range: IntRange = 0 .. 100): Arb< fun Arb.Companion.sequence(arb: Arb, range: IntRange = 0 .. 100): Arb> = Arb.list(arb, range).map { it.asSequence() } -fun Arb.Companion.functionAToB(arbB: Arb): Arb<(A) -> B> = arbitrary {random -> - { _: A -> arbB.next(random) }.memoize() -} +fun Arb.Companion.functionAToB(arb: Arb): Arb<(A) -> B> = + arb.map { b: B -> { _: A -> b } } -fun Arb.Companion.functionABCToD(arb: Arb): Arb<(A, B, C) -> D> = arbitrary {random -> +fun Arb.Companion.functionABCToD(arb: Arb): Arb<(A, B, C) -> D> = arbitrary { random -> ({ _: A, _:B, _:C -> arb.next(random)}.memoize()) } diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt index d7f83af65ac..51232cdb232 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt @@ -11,44 +11,27 @@ import io.kotest.property.arbitrary.take import io.kotest.property.assume import io.kotest.property.checkAll -class GeneratorsTest : FreeSpec( { - - "functionAToB" - { - "should return same result when invoked multiple times" { - checkAll(Arb.string(), Arb.functionAToB(Arb.int())) { a, fn -> - fn(a) shouldBe fn(a) - } - } - - "should return some different values" { - Arb.functionAToB(Arb.int()).take(100) - .forAtLeastOne { fn -> - checkAll(100, Arb.string(), Arb.string()) { - a,b -> - assume(a != b) - fn(a) shouldNotBe fn(b) - } - } - } - } - +class GeneratorsTest : FreeSpec({ "functionABCToD" - { "should return same result when invoked multiple times" { - checkAll(Arb.string(), Arb.string(), Arb.string(), Arb.functionABCToD(Arb.int())) { a, b, c, fn -> - fn(a,b,c) shouldBe fn(a,b,c) + checkAll( + Arb.string(), + Arb.string(), + Arb.string(), + Arb.functionABCToD(Arb.int()) + ) { a, b, c, fn -> + fn(a, b, c) shouldBe fn(a, b, c) } } "should return some different values" { Arb.functionABCToD(Arb.int()).take(100) - .forAtLeastOne { fn -> - checkAll(100, Arb.string(), Arb.string(), Arb.string()) { - a,b,c -> + .forAtLeastOne { fn -> + checkAll(100, Arb.string(), Arb.string(), Arb.string()) { a, b, c -> assume(a != c) - fn(a,b,c) shouldNotBe fn(c,b,a) + fn(a, b, c) shouldNotBe fn(c, b, a) } } } } - }) From 6aee2d7458bde1e82677da6a35b22091465657ab Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Sun, 19 Mar 2023 20:55:30 +0100 Subject: [PATCH 07/13] add better generator to create 2 or three maps that share some keys --- .../commonTest/kotlin/arrow/core/MapKTest.kt | 234 +++++++----------- .../kotlin/arrow/core/test/Generators.kt | 63 +++++ .../kotlin/arrow/core/test/GeneratorsTest.kt | 30 +++ 3 files changed, 182 insertions(+), 145 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt index e9e6c7fb2d9..2c51d39e6bd 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt @@ -6,6 +6,8 @@ import arrow.core.test.intSmall import arrow.core.test.ior import arrow.core.test.laws.MonoidLaws import arrow.core.test.longSmall +import arrow.core.test.map2 +import arrow.core.test.map3 import arrow.core.test.option import arrow.core.test.testLaws import arrow.typeclasses.Semigroup @@ -14,7 +16,6 @@ import io.kotest.inspectors.forAll import io.kotest.inspectors.forAllValues import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.collections.shouldBeIn -import io.kotest.matchers.ints.shouldBeGreaterThan import io.kotest.matchers.maps.shouldBeEmpty import io.kotest.matchers.maps.shouldContain import io.kotest.matchers.maps.shouldContainKey @@ -135,13 +136,17 @@ class MapKTest : StringSpec({ "can align maps" { // aligned keySet is union of a's and b's keys - checkAll(Arb.map(KEY_ARB, Arb.boolean()), Arb.map(KEY_ARB, Arb.boolean())) { a, b -> + checkAll( + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val aligned = a.align(b) aligned.size shouldBe (a.keys + b.keys).size } // aligned map contains Both for all entries existing in a and b - checkAll(Arb.map(KEY_ARB, Arb.boolean()), Arb.map(KEY_ARB, Arb.boolean())) { a, b -> + checkAll( + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val aligned = a.align(b) a.keys.intersect(b.keys).forEach { aligned[it]?.isBoth shouldBe true @@ -149,7 +154,9 @@ class MapKTest : StringSpec({ } // aligned map contains Left for all entries existing only in a - checkAll(Arb.map(KEY_ARB, Arb.boolean()), Arb.map(KEY_ARB, Arb.boolean())) { a, b -> + checkAll( + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val aligned = a.align(b) (a.keys - b.keys).forEach { key -> aligned[key]?.isLeft shouldBe true @@ -157,7 +164,9 @@ class MapKTest : StringSpec({ } // aligned map contains Right for all entries existing only in b - checkAll(Arb.map(KEY_ARB, Arb.boolean()), Arb.map(KEY_ARB, Arb.boolean())) { a, b -> + checkAll( + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val aligned = a.align(b) (b.keys - a.keys).forEach { key -> aligned[key]?.isRight shouldBe true @@ -167,7 +176,7 @@ class MapKTest : StringSpec({ "zip is idempotent" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall())) { + Arb.map(Arb.string(), Arb.intSmall())) { a -> a.zip(a) shouldBe a.mapValues { it.value to it.value } } @@ -175,7 +184,7 @@ class MapKTest : StringSpec({ "align is idempotent" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall())) { + Arb.map(Arb.string(), Arb.intSmall())) { a -> a.align(a) shouldBe a.mapValues { Ior.Both(it.value, it.value) } } @@ -183,11 +192,8 @@ class MapKTest : StringSpec({ "zip is commutative" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()) - ) { - a, - b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> a.zip(b) shouldBe b.zip(a).mapValues { it.value.second to it.value.first } } @@ -195,11 +201,8 @@ class MapKTest : StringSpec({ "align is commutative" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()) - ) { - a, - b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> a.align(b) shouldBe b.align(a).mapValues { it.value.swap() } } @@ -207,11 +210,8 @@ class MapKTest : StringSpec({ "zip is associative" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()) - ) { - a,b,c -> + Arb.map3(Arb.string(), Arb.int(), Arb.int(), Arb.int()) + ) { (a, b, c) -> fun Pair, C>.assoc(): Pair> = this.first.first to (this.first.second to this.second) @@ -222,11 +222,8 @@ class MapKTest : StringSpec({ "align is associative" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()) - ) { - a,b,c -> + Arb.map3(Arb.string(), Arb.int(), Arb.int(), Arb.int()) + ) { (a, b, c) -> fun Ior, C>.assoc(): Ior> = when (this) { @@ -249,32 +246,29 @@ class MapKTest : StringSpec({ "zip with" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()), - Arb.functionABCToD(Arb.string()) - ) { a, b, fn -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()), + Arb.functionABCToD(Arb.string()) + ) { (a, b), fn -> a.zip(b, fn) shouldBe a.zip(b).mapValues { fn(it.key, it.value.first, it.value.second) } } } "align with" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()), - Arb.functionAToB>, String>(Arb.string()) - ) { a, b, fn -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()), + Arb.functionAToB>, String>(Arb.string()) + ) { (a, b), fn -> a.align(b, fn) shouldBe a.align(b).mapValues { fn(it) } } } "zip functoriality" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()), - Arb.functionAToB(Arb.string()), - Arb.functionAToB(Arb.string()) + Arb.map2(Arb.string(), Arb.int(), Arb.int()), + Arb.functionAToB(Arb.string()), + Arb.functionAToB(Arb.string()) ) { - a,b,f,g -> + (a,b),f,g -> fun Pair.bimap(f: (A) -> B, g: (C) -> D) = Pair(f(first), g(second)) @@ -287,12 +281,11 @@ class MapKTest : StringSpec({ "align functoriality" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()), - Arb.functionAToB(Arb.string()), - Arb.functionAToB(Arb.string()) + Arb.map2(Arb.string(), Arb.int(), Arb.int()), + Arb.functionAToB(Arb.string()), + Arb.functionAToB(Arb.string()) ) { - a,b,f,g -> + (a,b),f,g -> val l = a.mapValues{ f(it.value)}.align(b.mapValues{g(it.value)}) val r = a.align(b).mapValues { it.value.bimap(f,g)} @@ -303,20 +296,18 @@ class MapKTest : StringSpec({ "alignedness" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()) - ) { - a ,b-> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> - fun toList(es: Map): List = - es.fold(emptyList()) { - acc, e -> acc + e.value - } + fun toList(es: Map): List = + es.fold(emptyList()) { acc, e -> + acc + e.value + } val left = toList(a) - fun Ior.toLeftOption() = - fold({it}, {null}, {a,_ -> a}) + fun Ior.toLeftOption() = + fold({ it }, { null }, { a, _ -> a }) // toListOf (folded . here) (align x y) val middle = toList(a.align(b).mapValues { it.value.toLeftOption() }).filterNotNull() @@ -355,10 +346,8 @@ class MapKTest : StringSpec({ "distributivity1" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()) - ) {x,y,z -> + Arb.map3(Arb.string(), Arb.string(), Arb.string(), Arb.string()) + ) {(x,y,z) -> fun Pair, Ior>.undistrThesePair(): Ior, C> = when (val l = this.first) { @@ -386,10 +375,8 @@ class MapKTest : StringSpec({ "distributivity2" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()) - ) {x,y,z -> + Arb.map3(Arb.string(), Arb.string(), Arb.string(), Arb.string()) + ) {(x,y,z) -> fun Pair, C>.distrPairThese(): Ior, Pair> = when (val l = this.first) { @@ -407,10 +394,8 @@ class MapKTest : StringSpec({ "distributivity3" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()) - ) {x,y,z -> + Arb.map3(Arb.string(), Arb.string(), Arb.string(), Arb.string()) + ) {(x,y,z) -> fun Ior, Pair>.undistrPairThese(): Pair, C> = when (val e = this) { @@ -483,9 +468,8 @@ class MapKTest : StringSpec({ "unalign is the inverse of align" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()) - ) { a,b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> a.align(b).unalign() shouldBe (a to b) } } @@ -502,9 +486,8 @@ class MapKTest : StringSpec({ "padZip" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()) - ) { a, b -> + Arb.map2(Arb.string(), Arb.string(), Arb.string()) + ) { (a, b) -> val x = a.padZip(b) a.forAll { @@ -523,20 +506,17 @@ class MapKTest : StringSpec({ "padZip with" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()), - Arb.functionABCToD(Arb.string()) - ) { a, b, fn -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()), + Arb.functionABCToD(Arb.string()) + ) { (a, b), fn -> a.padZip(b, fn) shouldBe a.padZip(b).mapValues { fn(it.key, it.value.first, it.value.second) } } } "salign" { checkAll( - Arb.map(KEY_ARB, Arb.string()), - Arb.map(KEY_ARB, Arb.string()) - ) { - a,b -> + Arb.map2(Arb.string(), Arb.string(), Arb.string()) + ) { (a, b) -> a.salign(Semigroup.string(), b) shouldBe a.align(b) {it.value.fold(::identity, ::identity) { a, b -> a + b } } } } @@ -595,48 +575,23 @@ class MapKTest : StringSpec({ } - "ensure that Arb used for map keys produces a small enough set of distinct values" { - - /* - when zipping/aligning maps we will execute different code paths depending if a given key is present in both maps or not. - when using types with lots of values like String/Int etc. this is most likely not the case. In effect the tests will not cover all code branches. - therefor we need to make sure to use an Arb here that produces a small enough set of distint values. - this test is to ensure that the arb in use should cause at least 50 iterations with at least 10 keys in both maps - */ - - val result = mutableListOf() - - val arb = KEY_ARB - checkAll( - Arb.map(arb, Arb.intSmall()), - Arb.map(arb, Arb.intSmall()) - ) { a, b -> - result.add((a.keys.intersect(b.keys)).size) - } - - result.count { it > 10 } shouldBeGreaterThan 50 - } - - - "zip2" { - checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()) - ) { a, b -> - val result = a.zip(b) { _, aa, bb -> Pair(aa, bb) } - val expected = a.filter { (k, _) -> b.containsKey(k) } - .map { (k, v) -> Pair(k, Pair(v, b[k]!!)) } - .toMap() + "zip2" { + checkAll( + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> + val result = a.zip(b) { _, aa, bb -> Pair(aa, bb) } + val expected = a.filter { (k, _) -> b.containsKey(k) } + .map { (k, v) -> Pair(k, Pair(v, b[k]!!)) } + .toMap() - result shouldBe expected - } + result shouldBe expected } + } "zip3" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()) - ) { a, b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val result = a.zip(b, b) { _, aa, bb, cc -> Triple(aa, bb, cc) } val expected = a.filter { (k, _) -> b.containsKey(k) } @@ -649,9 +604,8 @@ class MapKTest : StringSpec({ "zip4" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()), - ) { a, b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val result = a.zip(b, b, b) { _, aa, bb, cc, dd -> Tuple4(aa, bb, cc, dd) } val expected = a.filter { (k, _) -> b.containsKey(k) } @@ -664,9 +618,8 @@ class MapKTest : StringSpec({ "zip5" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()), - ) { a, b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val result = a.zip(b, b, b, b) { _, aa, bb, cc, dd, ee -> Tuple5(aa, bb, cc, dd, ee) } val expected = a.filter { (k, _) -> b.containsKey(k) } @@ -679,9 +632,8 @@ class MapKTest : StringSpec({ "zip6" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()), - ) { a, b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val result = a.zip(b, b, b, b, b) { _, aa, bb, cc, dd, ee, ff -> Tuple6(aa, bb, cc, dd, ee, ff) } val expected = a.filter { (k, _) -> b.containsKey(k) } @@ -694,9 +646,8 @@ class MapKTest : StringSpec({ "zip7" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()) - ) { a, b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val result = a.zip(b, b, b, b, b, b) { _, aa, bb, cc, dd, ee, ff, gg -> Tuple7(aa, bb, cc, dd, ee, ff, gg) } val expected = a.filter { (k, _) -> b.containsKey(k) } @@ -709,9 +660,8 @@ class MapKTest : StringSpec({ "zip8" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()) - ) { a, b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val result = a.zip(b, b, b, b, b, b, b) { _, aa, bb, cc, dd, ee, ff, gg, hh -> Tuple8(aa, bb, cc, dd, ee, ff, gg, hh) } @@ -725,9 +675,8 @@ class MapKTest : StringSpec({ "zip9" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()) - ) { a, b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val result = a.zip(b, b, b, b, b, b, b, b) { _, aa, bb, cc, dd, ee, ff, gg, hh, ii -> Tuple9( aa, @@ -752,9 +701,8 @@ class MapKTest : StringSpec({ "zip10" { checkAll( - Arb.map(KEY_ARB, Arb.intSmall()), - Arb.map(KEY_ARB, Arb.intSmall()) - ) { a, b -> + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val result = a.zip(b, b, b, b, b, b, b, b, b) { _, aa, bb, cc, dd, ee, ff, gg, hh, ii, jj -> Tuple10( aa, @@ -780,9 +728,8 @@ class MapKTest : StringSpec({ "flatMap" { checkAll( - Arb.map(Arb.string(), Arb.intSmall()), - Arb.map(Arb.string(), Arb.string()) - ) { a, b -> + Arb.map2(Arb.string(), Arb.int(), Arb.string()) + ) { (a, b) -> val result: Map = a.flatMap { b } val expected: Map = a.filter { (k, _) -> b.containsKey(k) } .map { (k, _) -> Pair(k, b[k]!!) } @@ -836,7 +783,4 @@ class MapKTest : StringSpec({ }) } } - }) - -private val KEY_ARB = Arb.int(0 .. 250) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt index 759077cc529..67071b4eb8e 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/Generators.kt @@ -27,7 +27,9 @@ import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.next import io.kotest.property.arbitrary.of import io.kotest.property.arbitrary.orNull +import io.kotest.property.arbitrary.pair import io.kotest.property.arbitrary.string +import io.kotest.property.arbitrary.triple import kotlinx.coroutines.Dispatchers import kotlin.math.max import kotlin.Result.Companion.failure @@ -141,3 +143,64 @@ suspend fun A.suspend(): A = COROUTINE_SUSPENDED } + +private fun value2(first: Arb, second: Arb): Arb> = + Arb.pair(first.orNull(.2), second.orNull(.2)) + +private fun value3(first: Arb, second: Arb, third: Arb): Arb> = + Arb.triple(first.orNull(.2), second.orNull(.2), third.orNull(.2)) + +private fun Map>.destructured(): Triple, Map, Map> { + val firstMap = mutableMapOf() + val secondMap = mutableMapOf() + val thirdMap = mutableMapOf() + + this.forEach { (key, triple) -> + val (a, b, c) = triple + + if (a != null) { + firstMap[key] = a + } + + if (b != null) { + secondMap[key] = b + } + + if (c != null) { + thirdMap[key] = c + } + } + + return Triple(firstMap, secondMap, thirdMap) +} + +private fun Map>.destructured(): Pair, Map> { + val firstMap = mutableMapOf() + val secondMap = mutableMapOf() + + this.forEach { (key, pair) -> + val (a, b) = pair + if (a != null) { + firstMap[key] = a + } + + if (b != null) { + secondMap[key] = b + } + } + + return firstMap to secondMap +} + +fun Arb.Companion.map2(arbK: Arb, arbA: Arb, arbB: Arb): Arb, Map>> = + Arb.map(arbK, value2(arbA, arbB)) + .map { it.destructured() } + +fun Arb.Companion.map3( + arbK: Arb, + arbA: Arb, + arbB: Arb, + arbC: Arb +): Arb, Map, Map>> = + Arb.map(arbK, value3(arbA, arbB, arbC)) + .map { it.destructured() } diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt index 51232cdb232..dfd2331b6d9 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt @@ -2,9 +2,13 @@ package arrow.core.test import io.kotest.core.spec.style.FreeSpec import io.kotest.inspectors.forAtLeastOne +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.ints.shouldBeZero import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.property.Arb +import io.kotest.property.RandomSource +import io.kotest.property.arbitrary.boolean import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.string import io.kotest.property.arbitrary.take @@ -34,4 +38,30 @@ class GeneratorsTest : FreeSpec({ } } } + + "Arb.map2" - { + val result = Arb.map2(Arb.string(), Arb.boolean(), Arb.boolean()) + .generate(RandomSource.default()).take(2000).map { it.value.first.keys.intersect(it.value.second.keys).size }.toList() + + "at least one sample should share no keys" { + result.forAtLeastOne { it.shouldBeZero() } + } + + "at least one sample should share some keys" { + result.forAtLeastOne { it.shouldBeGreaterThan(0) } + } + } + + "Arb.map3" - { + val result = Arb.map3(Arb.string(), Arb.boolean(), Arb.boolean(), Arb.boolean()) + .generate(RandomSource.default()).take(2000).map { it.value.first.keys.intersect(it.value.second.keys).size }.toList() + + "at least one sample should share no keys" { + result.forAtLeastOne { it.shouldBeZero() } + } + + "at least one sample should share some keys" { + result.forAtLeastOne { it.shouldBeGreaterThan(0) } + } + } }) From 52ad28aebb1b13db56155ec25662ea671cd3b8ec Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Sun, 19 Mar 2023 22:37:18 +0100 Subject: [PATCH 08/13] wip --- .../commonTest/kotlin/arrow/core/MapKTest.kt | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt index 2c51d39e6bd..4fb51638e57 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt @@ -16,6 +16,7 @@ import io.kotest.inspectors.forAll import io.kotest.inspectors.forAllValues import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.maps.shouldBeEmpty import io.kotest.matchers.maps.shouldContain import io.kotest.matchers.maps.shouldContainKey @@ -757,30 +758,20 @@ class MapKTest : StringSpec({ } result.shouldBeInstanceOf>>() - xs.forAll { - result.value.shouldContain(it.key to it.value.toString()) - } + + result.value shouldBe xs.mapValues { it.value.toString() } } } "mapOrAccumulate accumulates errors" { checkAll( - Arb.map(Arb.int(), Arb.boolean()) + Arb.map(Arb.int(), Arb.int()) ) { xs -> - val result: Either, Map> = xs.mapOrAccumulate { - if (it.value) { - raise(it.key) - } else { - "" - } - } - - result.fold({ nel -> - nel.all shouldBe xs.filter { it.value }.keys.toSet() - }, { - xs.shouldNotHaveValues(true) - }) + xs.mapOrAccumulate { + raise(it.value) + }.shouldBeInstanceOf>>() + .value.all.shouldContainAll(xs.values) } } }) From 700768decd330466a62b0ea89b45b0532bca22a4 Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Fri, 24 Mar 2023 11:37:39 +0100 Subject: [PATCH 09/13] review comment --- .../core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt index 1f0e70fdc12..d42bef901ef 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt @@ -391,9 +391,7 @@ public inline fun Map.filterIsInstance(): Map = * */ public fun Map.align(b: Map): Map> = - (keys + b.keys).mapNotNull { key -> - Ior.fromNullables(this[key], b[key])!!.let { key to it } - }.toMap() + (keys + b.keys).associateWith { key -> Ior.fromNullables(this[key], b[key])!! } /** * Combines two structures by taking the union of their shapes and combining the elements with the given function. From d9420768e0bceef45b66bed8682156da0594b252 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 24 Mar 2023 13:57:51 +0100 Subject: [PATCH 10/13] Remove randomness tests --- .../commonTest/kotlin/arrow/core/test/GeneratorsTest.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt index dfd2331b6d9..54b1f627787 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt @@ -2,13 +2,9 @@ package arrow.core.test import io.kotest.core.spec.style.FreeSpec import io.kotest.inspectors.forAtLeastOne -import io.kotest.matchers.ints.shouldBeGreaterThan -import io.kotest.matchers.ints.shouldBeZero import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.property.Arb -import io.kotest.property.RandomSource -import io.kotest.property.arbitrary.boolean import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.string import io.kotest.property.arbitrary.take @@ -39,6 +35,8 @@ class GeneratorsTest : FreeSpec({ } } + /* these tests may fail unexpectedly since they depend on randomness + "Arb.map2" - { val result = Arb.map2(Arb.string(), Arb.boolean(), Arb.boolean()) .generate(RandomSource.default()).take(2000).map { it.value.first.keys.intersect(it.value.second.keys).size }.toList() @@ -64,4 +62,5 @@ class GeneratorsTest : FreeSpec({ result.forAtLeastOne { it.shouldBeGreaterThan(0) } } } + */ }) From 9b0c06ee359fc0d6fadcd6577a0b1e6fa357f95d Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Fri, 24 Mar 2023 14:21:06 +0100 Subject: [PATCH 11/13] add test for filterInstance and fix bug --- .../arrow-core/src/commonMain/kotlin/arrow/core/map.kt | 2 +- .../src/commonTest/kotlin/arrow/core/MapKTest.kt | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt index f9358cdb8b3..96a9cfbe7cd 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt @@ -450,7 +450,7 @@ public fun Map>.filterOption(): Map = * Returns a Map containing all elements that are instances of specified type parameter R. */ public inline fun Map.filterIsInstance(): Map = - mapNotNull { it as? R } + mapNotNull { it.value as? R } /** * Combines two structures by taking the union of their shapes and using Ior to hold the elements. diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt index aad1bc53a5c..d2e29bc01bf 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt @@ -546,7 +546,7 @@ class MapKTest : StringSpec({ } } - "filterInstance" { + "filterInstance1" { checkAll( Arb.map(Arb.int(), Arb.choice(Arb.int(), Arb.string())) ) { xs -> @@ -557,6 +557,13 @@ class MapKTest : StringSpec({ } } + "filterInstance2" { + checkAll(Arb.map(Arb.int(), Arb.int())) { + xs -> + xs.filterIsInstance() shouldBe xs + } + } + "zip2" { checkAll( Arb.map2(Arb.string(), Arb.int(), Arb.int()) From 6fca2f46a43f819c811fb57340d584d7a82dfbc4 Mon Sep 17 00:00:00 2001 From: Alphonse Bendt <370821+abendt@users.noreply.github.com> Date: Fri, 24 Mar 2023 14:26:46 +0100 Subject: [PATCH 12/13] Update arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt Co-authored-by: Alejandro Serrano --- .../core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt index 96a9cfbe7cd..93f4849471d 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt @@ -450,7 +450,7 @@ public fun Map>.filterOption(): Map = * Returns a Map containing all elements that are instances of specified type parameter R. */ public inline fun Map.filterIsInstance(): Map = - mapNotNull { it.value as? R } + filterValues { it is R } /** * Combines two structures by taking the union of their shapes and using Ior to hold the elements. From c51aa234b0f907ed3af4b49c171dc02418c9d14d Mon Sep 17 00:00:00 2001 From: Alphonse Bendt Date: Fri, 24 Mar 2023 14:43:13 +0100 Subject: [PATCH 13/13] * fix filterIsInstance implementation * add test for filterIsInstance identity --- .../src/commonMain/kotlin/arrow/core/map.kt | 3 ++- .../src/commonTest/kotlin/arrow/core/MapKTest.kt | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt index 93f4849471d..74849db93bf 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/map.kt @@ -449,8 +449,9 @@ public fun Map>.filterOption(): Map = /** * Returns a Map containing all elements that are instances of specified type parameter R. */ +@Suppress("UNCHECKED_CAST") public inline fun Map.filterIsInstance(): Map = - filterValues { it is R } + filterValues { it is R } as Map /** * Combines two structures by taking the union of their shapes and using Ior to hold the elements. diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt index d2e29bc01bf..9bf275cabcc 100644 --- a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/MapKTest.kt @@ -546,7 +546,7 @@ class MapKTest : StringSpec({ } } - "filterInstance1" { + "filterInstance" { checkAll( Arb.map(Arb.int(), Arb.choice(Arb.int(), Arb.string())) ) { xs -> @@ -557,10 +557,15 @@ class MapKTest : StringSpec({ } } - "filterInstance2" { - checkAll(Arb.map(Arb.int(), Arb.int())) { - xs -> - xs.filterIsInstance() shouldBe xs + "filterInstance: identity" { + checkAll(Arb.map(Arb.int(), Arb.int())) { xs -> + xs.filterIsInstance() shouldBe xs + } + } + + "filterInstance: identity with null" { + checkAll(Arb.map(Arb.int(), Arb.int().orNull())) { xs -> + xs.filterIsInstance() shouldBe xs } }