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..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 = - mapNotNull { it as? 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 d4a27d4e0a1..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 @@ -1,19 +1,36 @@ 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 import arrow.core.test.longSmall -import arrow.core.test.nonEmptyList +import arrow.core.test.map2 +import arrow.core.test.map3 +import arrow.core.test.option import arrow.core.test.testLaws import arrow.typeclasses.Semigroup import io.kotest.core.spec.style.StringSpec -import io.kotest.property.Arb +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 +import io.kotest.matchers.maps.shouldNotContainKey +import io.kotest.matchers.nulls.shouldNotBeNull 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 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.orNull import io.kotest.property.arbitrary.string import io.kotest.property.checkAll @@ -66,16 +83,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() + } } } @@ -111,45 +135,444 @@ class MapKTest : StringSpec({ } } - "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 -> + "can align maps" { + checkAll( + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> val aligned = a.align(b) + // aligned keySet is union of a's and b's keys aligned.size shouldBe (a.keys + b.keys).size + // aligned map contains Both for all entries existing in a and b + a.keys.intersect(b.keys).forEach { + aligned[it]?.isBoth() shouldBe true + } + // aligned map contains Left for all entries existing only in a + (a.keys - b.keys).forEach { key -> + aligned[key]?.isLeft() shouldBe true + } + // aligned map contains Right for all entries existing only in b + (b.keys - a.keys).forEach { key -> + aligned[key]?.isRight() shouldBe true + } } } - "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 -> - val aligned = a.align(b) - a.keys.intersect(b.keys).forEach { - aligned[it]?.isBoth() shouldBe true + "zip is idempotent" { + checkAll( + Arb.map(Arb.string(), Arb.intSmall())) { + a -> + a.zip(a) shouldBe a.mapValues { it.value to it.value } + } + } + + "align is idempotent" { + checkAll( + Arb.map(Arb.string(), Arb.intSmall())) { + a -> + a.align(a) shouldBe a.mapValues { Ior.Both(it.value, it.value) } + } + } + + "zip is commutative" { + checkAll( + 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 } + } + } + + "align is commutative" { + checkAll( + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (a, b) -> + + a.align(b) shouldBe b.align(a).mapValues { it.value.swap() } + } + } + + "zip is associative" { + checkAll( + 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) + + a.zip(b.zip(c)) shouldBe (a.zip(b)).zip(c).mapValues { it.value.assoc() } + } + } + + "align is associative" { + checkAll( + Arb.map3(Arb.string(), Arb.int(), Arb.int(), Arb.int()) + ) { (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.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.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.map2(Arb.string(), Arb.int(), Arb.int()), + Arb.functionAToB(Arb.string()), + Arb.functionAToB(Arb.string()) + ) { + (a,b),f,g -> + + 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 + } + } + + "align functoriality" { + checkAll( + Arb.map2(Arb.string(), Arb.int(), Arb.int()), + 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.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (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())) { + 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.map3(Arb.string(), Arb.string(), Arb.string(), 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.map3(Arb.string(), Arb.string(), Arb.string(), 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.map3(Arb.string(), Arb.string(), Arb.string(), 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() } } } - "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 -> - val aligned = a.align(b) - (a.keys - b.keys).forEach { key -> - aligned[key]?.isLeft() shouldBe true + "unalign is the inverse of align" { + checkAll( + Arb.map2(Arb.string(), Arb.int(), Arb.int()) + ) { (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.map2(Arb.string(), Arb.string(), 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 } } } - "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 -> - val aligned = a.align(b) - (b.keys - a.keys).forEach { key -> - aligned[key]?.isRight() shouldBe true + "padZip with" { + checkAll( + 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.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 } } + } + } + + "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 + } + } + + "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 + } + } + "zip2" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), Arb.intSmall()) - ) { a, b -> + 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]!!)) } @@ -178,9 +601,8 @@ class MapKTest : StringSpec({ "zip3" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), 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) } @@ -210,9 +632,8 @@ class MapKTest : StringSpec({ "zip4" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), 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) } @@ -242,9 +663,8 @@ class MapKTest : StringSpec({ "zip5" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), 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) } @@ -274,9 +694,8 @@ class MapKTest : StringSpec({ "zip6" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), 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) } @@ -306,9 +725,8 @@ class MapKTest : StringSpec({ "zip7" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), 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) } @@ -338,9 +756,8 @@ class MapKTest : StringSpec({ "zip8" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), 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) } @@ -371,9 +788,8 @@ class MapKTest : StringSpec({ "zip9" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), 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, @@ -427,9 +843,8 @@ class MapKTest : StringSpec({ "zip10" { checkAll( - Arb.map(Arb.intSmall(), Arb.intSmall()), - Arb.map(Arb.intSmall(), 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, @@ -485,9 +900,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]!!) } @@ -496,20 +910,55 @@ class MapKTest : StringSpec({ } } - "flatMap with nullables" { - checkAll( - Arb.list(Arb.string(), 5..5), - Arb.list(Arb.intSmall(), 5..5), - Arb.list(Arb.string().orNull(), 5..5) - ) { keys, a, b -> - val mapA = keys.zip(a).toMap() - val mapB = keys.zip(b).toMap() - val result: Map = mapA.flatMap { mapB } - val expected: Map = mapA.filter { (k, _) -> mapB.containsKey(k) } - .map { (k, _) -> Pair(k, mapB[k]) } - .toMap() - result shouldBe expected + "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()) + ) { xs -> + + val result: Either, Map> = xs.mapOrAccumulate { + it.value.toString() + } + + result.shouldBeInstanceOf>>() + + result.value shouldBe xs.mapValues { it.value.toString() } } + } + "mapOrAccumulate accumulates errors" { + checkAll( + Arb.map(Arb.int(), Arb.int()) + ) { xs -> + + xs.mapOrAccumulate { + raise(it.value) + }.shouldBeInstanceOf>>() + .value.all.shouldContainAll(xs.values) + } + } + + "flatMap with nullables" { + checkAll( + Arb.list(Arb.string(), 5..5), + Arb.list(Arb.intSmall(), 5..5), + Arb.list(Arb.string().orNull(), 5..5) + ) { keys, a, b -> + val mapA = keys.zip(a).toMap() + val mapB = keys.zip(b).toMap() + val result: Map = mapA.flatMap { mapB } + val expected: Map = mapA.filter { (k, _) -> mapB.containsKey(k) } + .map { (k, _) -> Pair(k, mapB[k]) } + .toMap() + result shouldBe expected + } + } }) 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..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 @@ -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,9 +24,12 @@ 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.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 @@ -49,6 +54,10 @@ fun Arb.Companion.sequence(arb: Arb, range: IntRange = 0 .. 100): Arb 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 -> + ({ _: A, _:B, _:C -> arb.next(random)}.memoize()) +} + fun Arb.Companion.throwable(): Arb = Arb.of(listOf(RuntimeException(), NoSuchElementException(), IllegalArgumentException())) @@ -134,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 new file mode 100644 index 00000000000..54b1f627787 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/test/GeneratorsTest.kt @@ -0,0 +1,66 @@ +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({ + "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) + } + } + } + } + + /* 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() + + "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) } + } + } + */ +})