From 1d1d54036e388d1ee70f049cbeee29bd4c08dc93 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Mon, 10 Jul 2023 07:20:08 +0200 Subject: [PATCH 1/2] Add mapOrAccumulate extension in RaiseAccumulate (#3086) --- arrow-libs/core/arrow-core/api/arrow-core.api | 3 +++ .../arrow/core/raise/RaiseAccumulate.kt | 21 +++++++++++++++++++ .../arrow/core/raise/RaiseAccumulateSpec.kt | 18 ++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/RaiseAccumulateSpec.kt diff --git a/arrow-libs/core/arrow-core/api/arrow-core.api b/arrow-libs/core/arrow-core/api/arrow-core.api index e6a4a7ff19a..8f2d3fc0cac 100644 --- a/arrow-libs/core/arrow-core/api/arrow-core.api +++ b/arrow-libs/core/arrow-core/api/arrow-core.api @@ -3455,6 +3455,9 @@ public final class arrow/core/raise/Raise$DefaultImpls { public class arrow/core/raise/RaiseAccumulate : arrow/core/raise/Raise { public fun (Larrow/core/raise/Raise;)V + public final fun _mapOrAccumulate (Larrow/core/NonEmptyList;Lkotlin/jvm/functions/Function2;)Larrow/core/NonEmptyList; + public final fun _mapOrAccumulate (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)Ljava/util/List; + public final fun _mapOrAccumulate (Ljava/util/Set;Lkotlin/jvm/functions/Function2;)Ljava/util/Set; public fun attempt (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun bind (Larrow/core/Either;)Ljava/lang/Object; public fun bind (Larrow/core/Validated;)Ljava/lang/Object; diff --git a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt index f477e2b5306..8f85a58d77e 100644 --- a/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt +++ b/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/raise/RaiseAccumulate.kt @@ -619,6 +619,27 @@ public open class RaiseAccumulate( transform: RaiseAccumulate.(A) -> B ): NonEmptySet = raise.mapOrAccumulate(this, transform) + @RaiseDSL + @JvmName("_mapOrAccumulate") + public inline fun mapOrAccumulate( + iterable: Iterable, + transform: RaiseAccumulate.(A) -> B + ): List = raise.mapOrAccumulate(iterable, transform) + + @RaiseDSL + @JvmName("_mapOrAccumulate") + public inline fun mapOrAccumulate( + list: NonEmptyList, + transform: RaiseAccumulate.(A) -> B + ): NonEmptyList = raise.mapOrAccumulate(list, transform) + + @RaiseDSL + @JvmName("_mapOrAccumulate") + public inline fun mapOrAccumulate( + set: NonEmptySet, + transform: RaiseAccumulate.(A) -> B + ): NonEmptySet = raise.mapOrAccumulate(set, transform) + @RaiseDSL override fun Iterable>.bindAll(): List = mapOrAccumulate { it.bind() } diff --git a/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/RaiseAccumulateSpec.kt b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/RaiseAccumulateSpec.kt new file mode 100644 index 00000000000..64584c77789 --- /dev/null +++ b/arrow-libs/core/arrow-core/src/commonTest/kotlin/arrow/core/raise/RaiseAccumulateSpec.kt @@ -0,0 +1,18 @@ +package arrow.core.raise + +import arrow.core.NonEmptyList +import arrow.core.left +import arrow.core.nonEmptyListOf +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class RaiseAccumulateSpec : StringSpec({ + "RaiseAccumulate takes precedence over extension function" { + either, Int> { + zipOrAccumulate( + { ensure(false) { "false" } }, + { mapOrAccumulate(1..2) { ensure(false) { "$it: IsFalse" } } } + ) { _, _ -> 1 } + } shouldBe nonEmptyListOf("false", "1: IsFalse", "2: IsFalse").left() + } +}) From 9cfd73cf8955471a422c60027a6ff3142e2bf8a8 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 10 Jul 2023 10:54:18 +0200 Subject: [PATCH 2/2] Additional tests for `copy` in Optics (#3089) --- .../kotlin/arrow/optics/plugin/CopyTest.kt | 99 ++++++++++++++++ .../kotlin/arrow/optics/CopyTest.kt | 111 ++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 arrow-libs/optics/arrow-optics-ksp-plugin/src/test/kotlin/arrow/optics/plugin/CopyTest.kt create mode 100644 arrow-libs/optics/arrow-optics/src/commonTest/kotlin/arrow/optics/CopyTest.kt diff --git a/arrow-libs/optics/arrow-optics-ksp-plugin/src/test/kotlin/arrow/optics/plugin/CopyTest.kt b/arrow-libs/optics/arrow-optics-ksp-plugin/src/test/kotlin/arrow/optics/plugin/CopyTest.kt new file mode 100644 index 00000000000..b9cf3ef208d --- /dev/null +++ b/arrow-libs/optics/arrow-optics-ksp-plugin/src/test/kotlin/arrow/optics/plugin/CopyTest.kt @@ -0,0 +1,99 @@ +package arrow.optics.plugin + +import org.junit.jupiter.api.Test + +// from https://kotlinlang.slack.com/archives/C5UPMM0A0/p1688822411819599 +// and https://github.com/overfullstack/my-lab/blob/master/arrow/src/test/kotlin/ga/overfullstack/optics/OpticsLab.kt + +val copyCode = """ +@optics data class Person(val name: String, val age: Int, val address: Address) { + companion object +} +@optics data class Address(val street: Street, val city: City, val coordinates: List) { + companion object +} +@optics data class Street(val name: String, val number: Int?) { + companion object +} +@optics data class City(val name: String, val country: String) { + companion object +} + +fun Person.moveToAmsterdamCopy(): Person = copy { + Person.address.city.name set "Amsterdam" + Person.address.city.country set "Netherlands" + Person.address .coordinates set listOf(2, 3) +} + +fun Person.moveToAmsterdamInside(): Person = copy { + inside(Person.address.city) { + City.name set "Amsterdam" + City.country set "Netherlands" + } +} + +val me = + Person( + "Gopal", + 99, + Address(Street("Kotlinstraat", 1), City("Hilversum", "Netherlands"), listOf(1, 2)) + ) +""" + +class CopyTest { + @Test + fun `code compiles`() { + """ + |package PersonTest + |$imports + |$copyCode + """.compilationSucceeds() + } + + @Test + fun `birthday increments`() { + """ + |package PersonTest + |$imports + |$copyCode + |val meAfterBirthdayParty = Person.age.modify(me) { it + 1 } + |val r = Person.age.get(meAfterBirthdayParty) + """.evals("r" to 100) + } + + @Test + fun `moving to another city`() { + """ + |package PersonTest + |$imports + |$copyCode + |val newAddress = + | Address(Street("Kotlinplein", null), City("Amsterdam", "Netherlands"), listOf(1, 2)) + |val meAfterMoving = Person.address.set(me, newAddress) + |val r = Person.address.get(meAfterMoving).street.name + """.evals("r" to "Kotlinplein") + } + + @Test + fun `optics composition`() { + """ + |package PersonTest + |$imports + |$copyCode + |val personCity: Lens = Person.address compose Address.city compose City.name + |val meAtTheCapital = personCity.set(me, "Amsterdam") + |val r = meAtTheCapital.address.city.name + """.evals("r" to "Amsterdam") + } + + @Test + fun `optics copy to modify multiple fields`() { + """ + |package PersonTest + |$imports + |$copyCode + |val meAfterMoving = me.moveToAmsterdamInside() + |val r = meAfterMoving.address.city.name + """.evals("r" to "Amsterdam") + } +} diff --git a/arrow-libs/optics/arrow-optics/src/commonTest/kotlin/arrow/optics/CopyTest.kt b/arrow-libs/optics/arrow-optics/src/commonTest/kotlin/arrow/optics/CopyTest.kt new file mode 100644 index 00000000000..2426b4730ac --- /dev/null +++ b/arrow-libs/optics/arrow-optics/src/commonTest/kotlin/arrow/optics/CopyTest.kt @@ -0,0 +1,111 @@ +package arrow.optics + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +// from https://kotlinlang.slack.com/archives/C5UPMM0A0/p1688822411819599 +// and https://github.com/overfullstack/my-lab/blob/master/arrow/src/test/kotlin/ga/overfullstack/optics/OpticsLab.kt + +data class Person(val name: String, val age: Int, val address: Address) { + companion object { + val name: Lens = Lens( + get = { it.name }, + set = { p, x -> p.copy(name = x) } + ) + val age: Lens = Lens( + get = { it.age }, + set = { p, x -> p.copy(age = x) } + ) + val address: Lens = Lens( + get = { it.address }, + set = { p, x -> p.copy(address = x) } + ) + } +} +data class Address(val street: Street, val city: City, val coordinates: List) { + companion object { + val city: Lens = Lens( + get = { it.city }, + set = { a, x -> a.copy(city = x) } + ) + val coordinates: Lens> = Lens( + get = { it.coordinates }, + set = { a, x -> a.copy(coordinates = x) } + ) + } +} +data class Street(val name: String, val number: Int?) +data class City(val name: String, val country: String) { + companion object { + val name: Lens = Lens( + get = { it.name }, + set = { c, x -> c.copy(name = x) } + ) + val country: Lens = Lens( + get = { it.country }, + set = { c, x -> c.copy(country = x) } + ) + } +} + +fun Person.moveToAmsterdamCopy(): Person = copy { + Person.address + Address.city + City.name set "Amsterdam" + Person.address + Address.city + City.country set "Netherlands" + Person.address + Address.coordinates set listOf(2, 3) +} + +fun Person.moveToAmsterdamInside(): Person = copy { + inside(Person.address + Address.city) { + City.name set "Amsterdam" + City.country set "Netherlands" + } +} + +class CopyTest : StringSpec({ + "optics" { + val me = + Person( + "Gopal", + 99, + Address(Street("Kotlinstraat", 1), City("Hilversum", "Netherlands"), listOf(1, 2)) + ) + + Person.name.get(me) shouldBe "Gopal" + + val meAfterBirthdayParty = Person.age.modify(me) { it + 1 } + Person.age.get(meAfterBirthdayParty) shouldBe 100 + + val newAddress = + Address(Street("Kotlinplein", null), City("Amsterdam", "Netherlands"), listOf(1, 2)) + val meAfterMoving = Person.address.set(me, newAddress) + Person.address.get(meAfterMoving) shouldBe newAddress + } + + "optics composition" { + val personCity: Lens = Person.address compose Address.city compose City.name + + val me = + Person( + "Alejandro", + 35, + Address(Street("Kotlinstraat", 1), City("Hilversum", "Netherlands"), listOf(1, 2)) + ) + + personCity.get(me) shouldBe "Hilversum" + val meAtTheCapital = personCity.set(me, "Amsterdam") + meAtTheCapital.address.city.name shouldBe "Amsterdam" + } + + "optics copy to modify multiple fields" { + val me = + Person( + "Alejandro", + 35, + Address(Street("Kotlinstraat", 1), City("Hilversum", "Netherlands"), listOf(1, 2)) + ) + val meAfterMoving1 = me.moveToAmsterdamInside() + val meAfterMoving2 = me.moveToAmsterdamInside() + meAfterMoving1 shouldBe meAfterMoving2 + meAfterMoving1.address.city.name shouldBe "Amsterdam" + } +})