From 263b5b005b110c0bd730dc59cadb4f0b34268d58 Mon Sep 17 00:00:00 2001 From: Phil Davies Date: Tue, 14 May 2024 17:39:50 +0100 Subject: [PATCH 01/22] Mention ArrowModule in serialization and ktor sections --- content/docs/learn/integrations.md | 10 +++++++ .../docs/learn/quickstart/serialization.md | 29 +++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/content/docs/learn/integrations.md b/content/docs/learn/integrations.md index a8814bbc..0f518a1c 100644 --- a/content/docs/learn/integrations.md +++ b/content/docs/learn/integrations.md @@ -78,6 +78,16 @@ The main point of contact between Ktor and Arrow is in If you're using kotlinx.serialization, you need no further changes other than importing the serializers with `@UseSerializers`. +If you want to use Arrow Core types directly as your request or response models, you will need to include the `ArrowModule` in your serializers module: + +```kotlin +install(ContentNegotiation) { + json(Json { + serializersModule = ArrowModule + }) +} +``` + If you're using Jackson, you can use the [custom mapper](https://github.com/arrow-kt/arrow-integrations#ktor), and pass it to the `ContentNegotiation` configuration. diff --git a/content/docs/learn/quickstart/serialization.md b/content/docs/learn/quickstart/serialization.md index d3af6189..ba501d53 100644 --- a/content/docs/learn/quickstart/serialization.md +++ b/content/docs/learn/quickstart/serialization.md @@ -20,7 +20,7 @@ If you're using kotlinx.serialization, you need to depend on the Declare your serializable types as usual. However, when one of the fields mentions a type from Arrow Core, -``` +```kotlin @Serializable data class Book(val title: String, val authors: NonEmptyList) ``` @@ -28,11 +28,10 @@ data class Book(val title: String, val authors: NonEmptyList) you need to "import" the serializer into the file. The easiest way is to include a `UseSerializers` annotation at the very top. -``` +```kotlin @file:UseSerializers( EitherSerializer::class, IorSerializer::class, - ValidatedSerializer::class, OptionSerializer::class, NonEmptyListSerializer::class, NonEmptySetSerializer::class @@ -43,6 +42,30 @@ The list above mentions all the serializers, but you only need to add those which are used in your fields. Don't worry too much: if you miss one, the kotlinx.serialization plug-in gives you an error. +Additionally, you can use `arrow.core.serialization.ArrowModule` to register run-time [contextual serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#contextual-serialization) support of the Arrow Core types. + +```kotlin +val format = Json { serializersModule = ArrowModule } +``` + +or by merging with another serializers module + +```kotlin +val format = Json { serializersModule = myModule + ArrowModule } +``` + +This will allow for use of the Arrow Core types as the value to be serialized without specifying the serializer explicitly: + +```kotlin +val payload = format.encodeToString(nonEmptyListOf("hello", "world")) +``` + +:::note + +Using `@UseSerializers` to provide static _compile-time_ serialization of fields containing Arrow Core types is usually preferable to annotating fields with `@Contextual` and relying on _run-time_ resolution, wherever possible. + +::: + :::info Additional reading [_Marshalling Arrow Types in Ktor_](https://garthgilmour.medium.com/marshalling-arrow-types-in-ktor-bc471aa3650) From b9c81c0168331edd2df5d1bd7fceb14da432eb0e Mon Sep 17 00:00:00 2001 From: Phil Davies Date: Fri, 17 May 2024 12:39:55 +0100 Subject: [PATCH 02/22] remove kotlin tag on source blocks to avoid knit --- content/docs/learn/integrations.md | 4 ++-- content/docs/learn/quickstart/serialization.md | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/content/docs/learn/integrations.md b/content/docs/learn/integrations.md index 0f518a1c..9df92b41 100644 --- a/content/docs/learn/integrations.md +++ b/content/docs/learn/integrations.md @@ -80,7 +80,7 @@ importing the serializers with `@UseSerializers`. If you want to use Arrow Core types directly as your request or response models, you will need to include the `ArrowModule` in your serializers module: -```kotlin +``` install(ContentNegotiation) { json(Json { serializersModule = ArrowModule @@ -92,7 +92,7 @@ If you're using Jackson, you can use the [custom mapper](https://github.com/arrow-kt/arrow-integrations#ktor), and pass it to the `ContentNegotiation` configuration. -```kotlin +``` install(ContentNegotiation) { register(ContentType.Application.Json, JacksonConverter(JsonMapper.mapper)) } diff --git a/content/docs/learn/quickstart/serialization.md b/content/docs/learn/quickstart/serialization.md index ba501d53..4f68fc0b 100644 --- a/content/docs/learn/quickstart/serialization.md +++ b/content/docs/learn/quickstart/serialization.md @@ -20,7 +20,7 @@ If you're using kotlinx.serialization, you need to depend on the Declare your serializable types as usual. However, when one of the fields mentions a type from Arrow Core, -```kotlin +``` @Serializable data class Book(val title: String, val authors: NonEmptyList) ``` @@ -28,7 +28,7 @@ data class Book(val title: String, val authors: NonEmptyList) you need to "import" the serializer into the file. The easiest way is to include a `UseSerializers` annotation at the very top. -```kotlin +``` @file:UseSerializers( EitherSerializer::class, IorSerializer::class, @@ -44,19 +44,19 @@ kotlinx.serialization plug-in gives you an error. Additionally, you can use `arrow.core.serialization.ArrowModule` to register run-time [contextual serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#contextual-serialization) support of the Arrow Core types. -```kotlin +``` val format = Json { serializersModule = ArrowModule } ``` or by merging with another serializers module -```kotlin +``` val format = Json { serializersModule = myModule + ArrowModule } ``` This will allow for use of the Arrow Core types as the value to be serialized without specifying the serializer explicitly: -```kotlin +``` val payload = format.encodeToString(nonEmptyListOf("hello", "world")) ``` @@ -80,7 +80,7 @@ If you're using Jackson for serialization, [this module](https://github.com/arro adds support for Arrow types. You just need to call an additional method when creating the mapper. -```kotlin +``` val mapper = ObjectMapper() .registerKotlinModule() .registerArrowModule() // <- this is the one From 116718be314e574c33cdddcde0e2aba40ee9490a Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 29 May 2024 21:26:26 +0200 Subject: [PATCH 03/22] Arrow 2.0 release notes --- content/blog/2024-07-01-arrow-2-0.md | 115 +++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 content/blog/2024-07-01-arrow-2-0.md diff --git a/content/blog/2024-07-01-arrow-2-0.md b/content/blog/2024-07-01-arrow-2-0.md new file mode 100644 index 00000000..914e099e --- /dev/null +++ b/content/blog/2024-07-01-arrow-2-0.md @@ -0,0 +1,115 @@ +--- +title: Arrow 2.0 release +category: articles +tags: [core, articles] +--- + +# Arrow 2.0 release + +We are happy to announce the next major release of Arrow, version 2.0! +As previously announced, migrating your projects to this release should be hassle-free +if your code compiled in 1.2.x without any deprecation warnings +(except for the breaking change in optics generation discussed below). + +This release is built with the new K2 compiler, and this gives us the ability +to support a wider range of platforms, including WebAssembly. From now on, we shall +provide artifacts for every platform supported by Kotlin. + +Apart from stabilization and general bug fixing, the theme of this release +is improving the different DSLs provided by Arrow libraries. Our goal is to +empower developers to write more succinct and readable code. + +## Simple accumulation in Raise + +TODO: Talk about `accumulating` + +Using Arrow Core data types as part of serialized data requires additional integration. +In 1.2.x we started providing compile-time support for `kotlinx.serialization`. +From 2.0 we also provide `ArrowModule` for +[contextual serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#contextual-serialization). This is needed, among others, when the data is processed +by Ktor. + +## Additions to Fx + +Writing coroutine-heavy code may become cumbersome over time, especially if +one intends to use as much concurrency as possible. Arrow Fx includes a `parZip` +function, but not everybody enjoys having so many brackets. + +```kotlin +parZip( + { downloadFile() }, + { loadDataFromDatabase() } +) { file, data -> Result(file, data) } +``` + +The new `awaitAll` scope tries to improve the situation by tweaking the +usual `async` mechanism, ensuring that all `Deferred` values are `await`ed +once the first one is requested. That means that the previous code behaves +identically to the following. + +```kotlin +awaitAll { + val file = async { downloadFile() } + val data = async { loadDataFromDatabase() } + Result(file.await(), data.await()) +} +``` + +We've also improved the STM block by allowing delegation as a means to +read or change the value of a `TVar`. + +```kotlin +fun STM.deposit(accVar: TVar, amount: Int): Unit { + val acc by accVar // delegation here + val current = acc // implicit 'read' + acc = current + amount // implicit 'write' +} +``` + +## Clearer retries for particular exceptions + +Until now, the `retry` operation in the Resilience module would capture +any `Throwable` exception. From version 2.0 on you can specify a subclass +of `Throwable` to be the target for retrying, whereas the rest of +exceptions will bubble as usual. + +```kotlin +Schedule.recurs(2) + .retry { ... } +``` + +The subclass of exceptions must be given as a type argument. +Alas, Kotlin does not allow giving only a subset of those, and `retry` +has two type parameters (the second one represents the output type of +the `Schedule`). Fortunately, you can ask the compiler to infer the +second one using `_`. + +## Improved optics + +The two **breaking changes** in Arrow 2.0 relate to optics. +First of all, the optics hierarchy has been greatly simplified: +now we have traversals, optionals, lenses, prisms, and isos, and no more +intermediate types. This smaller amount of types means that the type of +optic compositions become easier to understand. + +We have also changed the generation of optics via the compiler plug-in +(that is, the `@optics` annotation) with respect to nullable fields. +In the 1.x series, a value of type `String?` would be presented as +`Optional`; this makes impossible to change the value from +`null` to an actual `String` using only optics operations. From version +2.0, that field is represented as `Lens`. To get the 1.x +behavior you should apply `.notNull` after the optic corresponding to +the field. + +One pain point when building traversals was the need to provide an +argument to `.every`, like `.every(Every.list())`. This new version +brings an improved variant that requires no arguments if the type +of the `Iterable` is known. Similar improvements have been applied +to `.at` and `.index`. + +One completely new feature in Arrow 2.0 is the _pattern matching_ DSL. +By combining prisms and lenses one can specify a complex shape, and +then check whether a value fits into that shape, extracting some +pieces of information on the go. The DSL gets quite close to pattern +matching found in functional languages like Haskell or Scala. The +interested reader may check the documentation. From c28c5a6f15f966dc8a341e9449264496ada46f6f Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 29 May 2024 21:35:33 +0200 Subject: [PATCH 04/22] Smaller updates to 2.0 --- content/docs/learn/coroutines/stm.md | 17 +++++++++++++++++ content/docs/learn/immutable-data/optional.md | 6 +++--- content/docs/learn/immutable-data/traversal.md | 14 +++++--------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/content/docs/learn/coroutines/stm.md b/content/docs/learn/coroutines/stm.md index 8a7f1a5a..17be3c40 100644 --- a/content/docs/learn/coroutines/stm.md +++ b/content/docs/learn/coroutines/stm.md @@ -88,6 +88,23 @@ One additional guarantee of STM is that the _whole_ transaction is executed atomically. That means we can modify several `TVar`s in one transaction, and we'll never observe an intermediate state. +:::tip Delegated transactional properties + +From version 2.0, you can use [property delegation](https://kotlinlang.org/docs/delegated-properties.html) +to access a `TVar`. That way you don't need explicit `read` or `write`, +they become implicit in the syntax. + +``` +fun STM.deposit(accVar: TVar, amount: Int): Unit { + val acc by accVar // property delegation + val current = acc // implicit 'read' + acc = current + amount // implicit 'write' + // or simply, acc = acc + amount +} +``` + +::: + ### Other STM data structures The following types are built upon `TVar`s and provided out of the box with Arrow: diff --git a/content/docs/learn/immutable-data/optional.md b/content/docs/learn/immutable-data/optional.md index bf5fcb43..3f6ae0ae 100644 --- a/content/docs/learn/immutable-data/optional.md +++ b/content/docs/learn/immutable-data/optional.md @@ -57,8 +57,8 @@ val db = Db(mapOf( )) fun example() { - Db.cities.index(Index.map(), "Alejandro").country.getOrNull(db) shouldBe "Netherlands" - Db.cities.index(Index.map(), "Jack").country.getOrNull(db) shouldBe null + Db.cities.index("Alejandro").country.getOrNull(db) shouldBe "Netherlands" + Db.cities.index("Jack").country.getOrNull(db) shouldBe null } ``` @@ -91,7 +91,7 @@ val db = Db(mapOf( ```kotlin fun example() { - val dbWithJack = Db.cities.index(Index.map(), "Jack").set(db, City("London", "UK")) + val dbWithJack = Db.cities.index("Jack").set(db, City("London", "UK")) // Jack was not really added to the database ("Jack" in dbWithJack.cities) shouldBe false } diff --git a/content/docs/learn/immutable-data/traversal.md b/content/docs/learn/immutable-data/traversal.md index d423488d..1c1b1301 100644 --- a/content/docs/learn/immutable-data/traversal.md +++ b/content/docs/learn/immutable-data/traversal.md @@ -75,20 +75,16 @@ and copying, and simply focuses on the path to access the values. ```kotlin fun Person.happyBirthdayFriendsOptics(): Person = - Person.friends.every(Every.list()).age.modify(this) { it + 1 } + Person.friends.every.age.modify(this) { it + 1 } ``` :::note Every(List) -You might be wondering why we need to write `Every.list()` as argument to `every`, -given that we know that `Person.friends` focuses on a `List`. -The reason is [type erasure](https://kotlinlang.org/docs/generics.html#type-erasure): -the compiler cannot differentiate between `Lens>` -and `Lens>`, so there's no way to tell which -is the right traversal to apply in each case. However, if we provide the -hint ourselves by giving `Every.list()` as argument, the compiler is able -to _check_ that our usage is correct. +In versions prior to 2.0, `.every` required an additional argument specifying +the type of traversal. So one would write `Person.friends.every(Every.list())`. +This problem was related to [type erasure](https://kotlinlang.org/docs/generics.html#type-erasure), +but fortunately, a new encoding was found without this caveat. ::: From 54dec11ddeb0e595cdd70924bb6de7c52c89c2f2 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 30 May 2024 07:21:24 +0200 Subject: [PATCH 05/22] Knit --- content/docs/learn/coroutines/stm.md | 14 ++++- .../kotlin/examples/example-optional-01.kt | 4 +- .../kotlin/examples/example-optional-02.kt | 2 +- .../test/kotlin/examples/example-stm-02.kt | 53 +++++++++++++++++++ .../kotlin/examples/example-traversal-01.kt | 2 +- 5 files changed, 70 insertions(+), 5 deletions(-) diff --git a/content/docs/learn/coroutines/stm.md b/content/docs/learn/coroutines/stm.md index 17be3c40..e7656679 100644 --- a/content/docs/learn/coroutines/stm.md +++ b/content/docs/learn/coroutines/stm.md @@ -94,7 +94,13 @@ From version 2.0, you can use [property delegation](https://kotlinlang.org/docs/ to access a `TVar`. That way you don't need explicit `read` or `write`, they become implicit in the syntax. -``` + + +```kotlin fun STM.deposit(accVar: TVar, amount: Int): Unit { val acc by accVar // property delegation val current = acc // implicit 'read' @@ -103,6 +109,12 @@ fun STM.deposit(accVar: TVar, amount: Int): Unit { } ``` + + + + ::: ### Other STM data structures diff --git a/guide/src/test/kotlin/examples/example-optional-01.kt b/guide/src/test/kotlin/examples/example-optional-01.kt index 8194beb4..889bc706 100644 --- a/guide/src/test/kotlin/examples/example-optional-01.kt +++ b/guide/src/test/kotlin/examples/example-optional-01.kt @@ -20,6 +20,6 @@ val db = Db(mapOf( )) fun example() { - Db.cities.index(Index.map(), "Alejandro").country.getOrNull(db) shouldBe "Netherlands" - Db.cities.index(Index.map(), "Jack").country.getOrNull(db) shouldBe null + Db.cities.index("Alejandro").country.getOrNull(db) shouldBe "Netherlands" + Db.cities.index("Jack").country.getOrNull(db) shouldBe null } diff --git a/guide/src/test/kotlin/examples/example-optional-02.kt b/guide/src/test/kotlin/examples/example-optional-02.kt index 0f5cca4a..01b1d1b7 100644 --- a/guide/src/test/kotlin/examples/example-optional-02.kt +++ b/guide/src/test/kotlin/examples/example-optional-02.kt @@ -21,7 +21,7 @@ val db = Db(mapOf( )) fun example() { - val dbWithJack = Db.cities.index(Index.map(), "Jack").set(db, City("London", "UK")) + val dbWithJack = Db.cities.index("Jack").set(db, City("London", "UK")) // Jack was not really added to the database ("Jack" in dbWithJack.cities) shouldBe false } diff --git a/guide/src/test/kotlin/examples/example-stm-02.kt b/guide/src/test/kotlin/examples/example-stm-02.kt index c2ff5cc0..305acfc6 100644 --- a/guide/src/test/kotlin/examples/example-stm-02.kt +++ b/guide/src/test/kotlin/examples/example-stm-02.kt @@ -5,6 +5,59 @@ package arrow.website.examples.exampleStm02 import io.kotest.assertions.fail import io.kotest.matchers.shouldBe +import arrow.fx.stm.atomically +import arrow.fx.stm.TVar +import arrow.fx.stm.STM +---> + +```kotlin +fun STM.deposit(accVar: TVar, amount: Int): Unit { + val acc by accVar // property delegation + val current = acc // implicit 'read' + acc = current + amount // implicit 'write' + // or simply, acc = acc + amount +} +``` + + + + + +::: + +### Other STM data structures + +The following types are built upon `TVar`s and provided out of the box with Arrow: + +- [`TQueue`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-queue/index.html): transactional mutable queue, +- [`TMVar`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-m-var/index.html): mutable transactional variable that may be empty, +- [`TSet`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-set/index.html), [`TMap`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-map/index.html): transactional `Set` and `Map`, +- [`TArray`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-array/index.html): array of `TVar`s, +- [`TSemaphore`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-semaphore/index.html): transactional semaphore. + +:::tip + +Note that, in most cases, using these data structures is much more efficient than +wrapping their "regular" version in a `TVar`. For example, a `TSet` performs +better than a `TVar>` because the latter needs to "lock" the entire set +on modification, whereas the former knows that only the affected entries need +to be taken into account. + +::: + +## Retries + +It is sometimes beneficial to manually abort the current transaction if an +invalid state has been reached. For example, a `TQueue` had no elements to read. +The aborted transaction will automatically restart once any previously accessed +variable has changed. + +Here in this example, we've changed `withdraw` to use `retry` and thus wait until +enough money is in the account, which after a few seconds happens to be the case. + + +```kotlin +suspend fun getUser(id: UserId): User = awaitAll { + val name = async { getUserName(id) } + val avatar = async { getAvatar(id) } + User(name.await(), avatar.await()) +} +``` + + +As the name suggests, within this `awaitAll` block, every time you call `.await()` +_all_ of the `async` computations that were registered until that point are +awaited. If any of those throws an exception, the whole block is canceled, as +per the rules of structured concurrency. In general, writing a sequence of independent +`async` computations within `awaitAll` is equivalent to giving those computations +as arguments to `parZip`. + ### Flows The [`parMap`](https://apidocs.arrow-kt.io/arrow-fx-coroutines/arrow.fx.coroutines/par-map.html) @@ -113,7 +144,7 @@ suspend fun file(server1: String, server2: String) = { downloadFrom(server2) } ).merge() ``` - + The example above shows a typical pattern combined with `raceN`. The result of the function above is `Either`, with each type @@ -156,7 +187,7 @@ suspend fun example() { println(triple) } ``` - + ```text Sleeping for 500 milliseconds ... @@ -210,7 +241,7 @@ suspend fun example() { println(res) } ``` - + In the output, we can see that tasks `1` and `3` started, but `2` _raised_ an error that triggered the cancellation of the other two tasks. After tasks `1` and `3` are canceled, we see that the result of `raise` is returned and prints the error message. @@ -255,7 +286,7 @@ suspend fun example() { println(res) } ``` - + The example transforms, or maps, every element of an `Iterable` `[1, 2, 3, 4]` in _parallel_ using `parMap` and `failOnEven`. Since `failOnEven` raises an error when the `Int` is even, it fails for inputs 2 and 4, and the other two coroutines are canceled. diff --git a/guide/src/test/kotlin/examples/example-parallel-03.kt b/guide/src/test/kotlin/examples/example-parallel-03.kt index c786bd9c..79378ab1 100644 --- a/guide/src/test/kotlin/examples/example-parallel-03.kt +++ b/guide/src/test/kotlin/examples/example-parallel-03.kt @@ -1,12 +1,15 @@ // This file was automatically generated from parallel.md by Knit tool. Do not edit. package arrow.website.examples.exampleParallel03 -import arrow.core.merge -import arrow.fx.coroutines.raceN -suspend fun downloadFrom(url: String): Unit = Unit +import arrow.fx.coroutines.parZip +import arrow.fx.coroutines.await.awaitAll +typealias UserId = Int +data class User(val name: String, val avatar: String) +suspend fun getUserName(id: UserId): String = "$id-name" +suspend fun getAvatar(id: UserId): String = "$id-avatar" -suspend fun file(server1: String, server2: String) = - raceN( - { downloadFrom(server1) }, - { downloadFrom(server2) } - ).merge() +suspend fun getUser(id: UserId): User = awaitAll { + val name = async { getUserName(id) } + val avatar = async { getAvatar(id) } + User(name.await(), avatar.await()) +} diff --git a/guide/src/test/kotlin/examples/example-parallel-04.kt b/guide/src/test/kotlin/examples/example-parallel-04.kt index fc8c0f87..ab8e4119 100644 --- a/guide/src/test/kotlin/examples/example-parallel-04.kt +++ b/guide/src/test/kotlin/examples/example-parallel-04.kt @@ -1,24 +1,12 @@ // This file was automatically generated from parallel.md by Knit tool. Do not edit. package arrow.website.examples.exampleParallel04 -import arrow.core.raise.either -import arrow.fx.coroutines.parZip -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.delay +import arrow.core.merge +import arrow.fx.coroutines.raceN +suspend fun downloadFrom(url: String): Unit = Unit -suspend fun logCancellation(): Unit = try { - println("Sleeping for 500 milliseconds ...") - delay(500) -} catch (e: CancellationException) { - println("Sleep was cancelled early!") - throw e -} - -suspend fun example() { - val triple = parZip( - { either { logCancellation() } }, - { either { delay(100); raise("Error") } }, - { either { logCancellation() } } - ) { a, b, c -> Triple(a, b, c) } - println(triple) -} +suspend fun file(server1: String, server2: String) = + raceN( + { downloadFrom(server1) }, + { downloadFrom(server2) } + ).merge() diff --git a/guide/src/test/kotlin/examples/example-parallel-05.kt b/guide/src/test/kotlin/examples/example-parallel-05.kt index 81d42aab..6583e4a2 100644 --- a/guide/src/test/kotlin/examples/example-parallel-05.kt +++ b/guide/src/test/kotlin/examples/example-parallel-05.kt @@ -1,12 +1,10 @@ // This file was automatically generated from parallel.md by Knit tool. Do not edit. package arrow.website.examples.exampleParallel05 -import kotlinx.coroutines.delay -import arrow.core.raise.Raise import arrow.core.raise.either import arrow.fx.coroutines.parZip -import kotlin.time.Duration.Companion.milliseconds -import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay suspend fun logCancellation(): Unit = try { println("Sleeping for 500 milliseconds ...") @@ -17,12 +15,10 @@ suspend fun logCancellation(): Unit = try { } suspend fun example() { - val res = either { - parZip( - { logCancellation() } , - { delay(100); raise("Error") }, - { logCancellation() } - ) { a, b, c -> Triple(a, b, c) } - } - println(res) + val triple = parZip( + { either { logCancellation() } }, + { either { delay(100); raise("Error") } }, + { either { logCancellation() } } + ) { a, b, c -> Triple(a, b, c) } + println(triple) } diff --git a/guide/src/test/kotlin/examples/example-parallel-06.kt b/guide/src/test/kotlin/examples/example-parallel-06.kt index 4b742da6..fc26b797 100644 --- a/guide/src/test/kotlin/examples/example-parallel-06.kt +++ b/guide/src/test/kotlin/examples/example-parallel-06.kt @@ -4,8 +4,8 @@ package arrow.website.examples.exampleParallel06 import kotlinx.coroutines.delay import arrow.core.raise.Raise import arrow.core.raise.either -import arrow.core.raise.ensure -import arrow.fx.coroutines.parMap +import arrow.fx.coroutines.parZip +import kotlin.time.Duration.Companion.milliseconds import kotlin.coroutines.cancellation.CancellationException suspend fun logCancellation(): Unit = try { @@ -16,14 +16,13 @@ suspend fun logCancellation(): Unit = try { throw e } -suspend fun Raise.failOnEven(i: Int): Unit { - ensure(i % 2 != 0) { delay(100); "Error" } - logCancellation() -} - suspend fun example() { val res = either { - listOf(1, 2, 3, 4).parMap { failOnEven(it) } + parZip( + { logCancellation() } , + { delay(100); raise("Error") }, + { logCancellation() } + ) { a, b, c -> Triple(a, b, c) } } println(res) } diff --git a/guide/src/test/kotlin/examples/example-parallel-07.kt b/guide/src/test/kotlin/examples/example-parallel-07.kt new file mode 100644 index 00000000..5e7a34f0 --- /dev/null +++ b/guide/src/test/kotlin/examples/example-parallel-07.kt @@ -0,0 +1,29 @@ +// This file was automatically generated from parallel.md by Knit tool. Do not edit. +package arrow.website.examples.exampleParallel07 + +import kotlinx.coroutines.delay +import arrow.core.raise.Raise +import arrow.core.raise.either +import arrow.core.raise.ensure +import arrow.fx.coroutines.parMap +import kotlin.coroutines.cancellation.CancellationException + +suspend fun logCancellation(): Unit = try { + println("Sleeping for 500 milliseconds ...") + delay(500) +} catch (e: CancellationException) { + println("Sleep was cancelled early!") + throw e +} + +suspend fun Raise.failOnEven(i: Int): Unit { + ensure(i % 2 != 0) { delay(100); "Error" } + logCancellation() +} + +suspend fun example() { + val res = either { + listOf(1, 2, 3, 4).parMap { failOnEven(it) } + } + println(res) +} diff --git a/guide/src/test/kotlin/examples/test/ParallelTest.kt b/guide/src/test/kotlin/examples/test/ParallelTest.kt index ebfc41c1..e65ab092 100644 --- a/guide/src/test/kotlin/examples/test/ParallelTest.kt +++ b/guide/src/test/kotlin/examples/test/ParallelTest.kt @@ -7,8 +7,8 @@ import arrow.website.captureOutput import kotlinx.knit.test.verifyOutputLines class ParallelTest { - @Test fun ExampleParallel04() = runTest { - captureOutput("ExampleParallel04") { arrow.website.examples.exampleParallel04.example() } + @Test fun ExampleParallel05() = runTest { + captureOutput("ExampleParallel05") { arrow.website.examples.exampleParallel05.example() } .verifyOutputLines( "Sleeping for 500 milliseconds ...", "Sleeping for 500 milliseconds ...", @@ -16,8 +16,8 @@ class ParallelTest { ) } - @Test fun ExampleParallel05() = runTest { - captureOutput("ExampleParallel05") { arrow.website.examples.exampleParallel05.example() } + @Test fun ExampleParallel06() = runTest { + captureOutput("ExampleParallel06") { arrow.website.examples.exampleParallel06.example() } .verifyOutputLines( "Sleeping for 500 milliseconds ...", "Sleeping for 500 milliseconds ...", @@ -27,8 +27,8 @@ class ParallelTest { ) } - @Test fun ExampleParallel06() = runTest { - captureOutput("ExampleParallel06") { arrow.website.examples.exampleParallel06.example() } + @Test fun ExampleParallel07() = runTest { + captureOutput("ExampleParallel07") { arrow.website.examples.exampleParallel07.example() } .verifyOutputLines( "Sleeping for 500 milliseconds ...", "Sleeping for 500 milliseconds ...", From b267808444f7213bb2dd0939c596fbf1e9e3a04e Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 31 May 2024 17:05:03 +0200 Subject: [PATCH 07/22] Pattern matching DSL --- content/docs/learn/immutable-data/matching.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 content/docs/learn/immutable-data/matching.md diff --git a/content/docs/learn/immutable-data/matching.md b/content/docs/learn/immutable-data/matching.md new file mode 100644 index 00000000..0539823c --- /dev/null +++ b/content/docs/learn/immutable-data/matching.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 7 +--- + +# (Pattern) matching + +[Optionals](optional.md) and [prisms](prism-iso.md) provide ways to query +whether a certain piece of data is present, and [lenses](lens.md) allow +us to extract some information from a value. In many functional languages, +those tasks are accomplished using [pattern matching](https://en.wikipedia.org/wiki/Pattern_matching); +and with Arrow Optics those same ideas translate to Kotlin. + + + + + +In the following examples we use this set of types, which showcase a +sealed hierarchy (`User`/`Person`/`Company`) for which prisms are generated, +and data classes for which we get lenses. + +```kotlin +@optics data class Name( + val firstName: String, val lastName: String +) { companion object } + +@optics sealed interface User { companion object } +@optics data class Person( + val name: Name, val age: Int +): User { companion object } +@optics data class Company( + val name: String, val director: Name, val address: String +): User +``` \ No newline at end of file From fbfe9d13736b87caebc948cdcc677f0cf28354cb Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Tue, 4 Jun 2024 21:03:34 +0200 Subject: [PATCH 08/22] More on pattern matching --- content/docs/learn/immutable-data/matching.md | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/content/docs/learn/immutable-data/matching.md b/content/docs/learn/immutable-data/matching.md index 0539823c..fbc1d92e 100644 --- a/content/docs/learn/immutable-data/matching.md +++ b/content/docs/learn/immutable-data/matching.md @@ -18,7 +18,7 @@ import arrow.optics.* import arrow.optics.match.* --> -In the following examples we use this set of types, which showcase a +In the rest of this document we use this set of types, which showcase a sealed hierarchy (`User`/`Person`/`Company`) for which prisms are generated, and data classes for which we get lenses. @@ -34,4 +34,68 @@ and data classes for which we get lenses. @optics data class Company( val name: String, val director: Name, val address: String ): User -``` \ No newline at end of file +``` + +Here is the implementation of a small function that returns how to show +the name of a `User`. We use pattern matching to extract the information; +in particular, we pattern match on `this`, as hinted by the `this.matchOrThrow` +heading the code. + +```kotlin +val User.name: String get() = this.matchOrThrow { + // Company(name = nm, director = Name(lastName = d)) + User.company(Company.name, Company.director(Name.lastName)) then { (nm, d) -> "$nm, att. $d" } + // Person(Name(firstName = fn), age if it < 18) + User.person(Person.name(Name.firstName), Person.age.suchThat { it < 18 }) then { (fn, _) -> fn } + // Person(Name(firstName = fn, lastName = ln)) -> "Sir/Madam $fn $ln" + User.person(Person.name(Name.firstName, Name.lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } +} +``` + +Let us unpack the block right after `this.matchOrThrow`, which defines three +potential _cases_. Each case is made up of two elements separated by `then`: + +1. The _pattern_ describes a shape of data that the value should be + matched against; +2. The _body_ describes the code to execute if the match is successful. + In that body, you have access to pieces of the data from the pattern. + +One way to describe a pattern is using a diagram, where each node represents a value of a type, +and each edge represents a lens or prism to be applied to a value. The diagram corresponding +to our first case looks as follows. + +```mermaid +flowchart TD + start[User] + company[Company] + name[String → nm] + director[Name] + lastName[String → d] + start -->|"User.company"| company + company -->|"Company.name"| name + company -->|"Company.director"| director + director --> |"Name.lastName"| lastName +``` + +As you can see, you can match more than one field inside a value by joining different optics +inside parentheses. This is what we do above with the name of the company, and the last name +of the director. After digging into the value, you always reach some _terminal_ values, +which are represented in the diagram as nodes with no children. Those terminal values are the +ones available as arguments to the body of the case; above we show the corresponding variable +names in the body. + +The pattern matching mechanism in `arrow-match` allows you to add checks about values +at any point in the pattern. In our example above, the `age` is checked to decide whether to +apply the first match, using `.suchThat { it < 18 }`. + +:::tip It + +If you do not need to perform an initial check with a prism, as we do above, you use +`it` at the start of the pattern. For example, `it(Person.name(Person.firstName))` would +match on a `Person` and obtain its first name. + +::: + +## Default cases + + From 183ffe3f1c4056ac28e24ba283e1ed6f60168b8e Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Tue, 1 Oct 2024 13:52:52 +0200 Subject: [PATCH 09/22] Finish pattern matching section --- content/docs/learn/immutable-data/matching.md | 84 ++++++++++++++++++- content/docs/learn/overview.md | 3 +- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/content/docs/learn/immutable-data/matching.md b/content/docs/learn/immutable-data/matching.md index fbc1d92e..de1ddedc 100644 --- a/content/docs/learn/immutable-data/matching.md +++ b/content/docs/learn/immutable-data/matching.md @@ -47,7 +47,7 @@ val User.name: String get() = this.matchOrThrow { User.company(Company.name, Company.director(Name.lastName)) then { (nm, d) -> "$nm, att. $d" } // Person(Name(firstName = fn), age if it < 18) User.person(Person.name(Name.firstName), Person.age.suchThat { it < 18 }) then { (fn, _) -> fn } - // Person(Name(firstName = fn, lastName = ln)) -> "Sir/Madam $fn $ln" + // Person(Name(firstName = fn, lastName = ln)) User.person(Person.name(Name.firstName, Name.lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } } ``` @@ -84,7 +84,7 @@ which are represented in the diagram as nodes with no children. Those terminal v ones available as arguments to the body of the case; above we show the corresponding variable names in the body. -The pattern matching mechanism in `arrow-match` allows you to add checks about values +The pattern matching mechanism in `arrow-optics` also allow you to add checks about values at any point in the pattern. In our example above, the `age` is checked to decide whether to apply the first match, using `.suchThat { it < 18 }`. @@ -98,4 +98,84 @@ match on a `Person` and obtain its first name. ## Default cases +The example at the beginning of this section is _exhaustive_, that is, matches every possible +value of type `User`. This may not always be the case; using `matchOrThrow` over a value which +is not handled by any pattern results in a `MatchNotFound` exception. +If you want to define how to handle any remaining case, you use use the special `default` pattern. +For example, here we gather information about the director if available, and return `null` otherwise. + +```kotlin +fun User.directorOrNull(): Name? = this.matchOrThrow { + User.company(Company.director) then { it } + default { null } +} +``` + +:::warning Exhaustiveness checking + +It is responsibility of the developer to ensure that the patterns cover every possible case, +adding `default` whenever required. + +The Kotlin compiler ensures that every `when` with a subject is ehxaustive using a specialized +analysis. Unfortunately, such an analysis is not performed over the patterns described in +this section. + +::: + +## Matching without optics + +You can also use pattern matching without bringing the full optics machinery in, by using +the reflection facilities provided by the Kotlin language alongisde the `arrow-match` package. + +:::tip Type-safe reflection + +Even though the term _reflection_ usually evokes unsafety, the API provided by `arrow-match` +guarantees that patterns that compile are well-behaved. + +::: + +Since we do not have optics, we need another way to represent types and property: + +- Instead of prisms, the type is chosen using `T::class`; +- Instead of lenses, properties are described using `T::property`. +- Further matching on a value is done using `.of`. + +You can see those changes in action in the "translation" of the first example in this section +to `arrow-match`. + +```kotlin +val User.name: String get() = this.matchOrThrow { + // Company(name = nm, director = Name(lastName = d)) + Company::class.of(Company::name, Company::director.of(Name::lastName)) then { (nm, d) -> "$nm, att. $d" } + // Person(Name(firstName = fn), age if it < 18) + Person::class.of(Person::name.of(Name::firstName), Person::age.suchThat { it < 18 }) then { (fn, _) -> fn } + // Person(Name(firstName = fn, lastName = ln)) + Person::class.of(Person::name.of(Name::firstName, Name::lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } +} +``` + +## Matching with guards + +We strongly discourage using Arrow's pattern matching facilities for simple cases. +In most cases patterns can be recreated using `when` expressions with guards and +property access, leading to more idiomatic code. This approach has the additional +benefit of the exhaustiveness checking performed by the compiler. + +```kotlin +val User.name: String get() = when (this) { + is Company -> "$name, att ${director.lastName}" + is Person if age < 18 -> name.firstName + is Person -> "Sir/Madam ${name.firstName} ${name.lastName}" +} +``` + +Here are some rules of thumb to decide when pattern matching is a good solution: + +- The decision over which branch to takes depends on nested information. + This is the case, for example, when working with abstract syntax trees (ASTs). +- One needs to obtain several sub-values. For example, `User.name` defined without + pattern matching forces us to write `name.firstName` and `name.lastName`, whereas + this duplication is gone when using a pattern. + +As you can see, the more nested your data it, the more pattern matching helps. diff --git a/content/docs/learn/overview.md b/content/docs/learn/overview.md index 06455d99..35921754 100644 --- a/content/docs/learn/overview.md +++ b/content/docs/learn/overview.md @@ -36,7 +36,8 @@ Each section in the documentation roughly corresponds to one of the libraries th | `arrow-fx-coroutines`
_Companion to [KotlinX Coroutines](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/)_ | [High-level concurrency](../coroutines/parallel), including `parMap` and `parZip`
[Resource management](../coroutines/resource-safety/) (with `suspend`) | | `arrow-autoclose` | [Resource management](../coroutines/resource-safety/) (no `suspend`) | | `arrow-resilience` | [Resilience patterns](../resilience/) | -| `arrow-optics` + `arrow-optics-ksp-plugin`
_Companion to [data](https://kotlinlang.org/docs/data-classes.html) and [sealed](https://kotlinlang.org/docs/sealed-classes.html) classes_ | Utilities for [immutable data](../immutable-data/intro/) | +| `arrow-optics` + `arrow-optics-ksp-plugin`
_Companion to [data](https://kotlinlang.org/docs/data-classes.html) and [sealed](https://kotlinlang.org/docs/sealed-classes.html) classes_ | Utilities for [immutable data](../immutable-data/intro/), including [pattern matching](../immutable-data/matching/) | +| `arrow-match` | [Pattern matching](../immutable-data/matching/) without optics | | `arrow-fx-stm` | [Software Transactional Memory](../coroutines/stm/) (STM) | | `arrow-atomic`
_Multiplatform-ready [references](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-atomic-reference/)_ | [Atomic references](../coroutines/concurrency-primitives/#atomic) | | `arrow-collectors`
_Companion to [`fold`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/fold.html) and [`reduce`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/reduce.html)_ | [Aggregation with single traversal](../collections-functions/collectors/) | From a58845f921307c9317cedef272325afe7f41e5d0 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Tue, 1 Oct 2024 16:57:44 +0200 Subject: [PATCH 10/22] Talk about accumulating --- content/blog/2024-07-01-arrow-2-0.md | 83 +++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/content/blog/2024-07-01-arrow-2-0.md b/content/blog/2024-07-01-arrow-2-0.md index 914e099e..74beec26 100644 --- a/content/blog/2024-07-01-arrow-2-0.md +++ b/content/blog/2024-07-01-arrow-2-0.md @@ -19,15 +19,51 @@ Apart from stabilization and general bug fixing, the theme of this release is improving the different DSLs provided by Arrow libraries. Our goal is to empower developers to write more succinct and readable code. +* [Simple accumulation in Raise](#simple-accumulation-in-raise) +* [Additions to Fx](#additions-to-fx) +* [Clearer retries for particular exceptions](#clearer-retries-for-particular-exceptions) +* [Improved optics](#improved-optics) +* [Pattern matching](#pattern-matching) +* [Better support for kotlinx.serialization](#better-support-for-kotlinxserialization) + ## Simple accumulation in Raise -TODO: Talk about `accumulating` +One of the core concepts when working with typed errors is the distinction +between fail-first and accumulation of errors. Until now, the latter mode +required using `parZip` and `parMap`, which sometimes obscure the actual +flow of the computation. -Using Arrow Core data types as part of serialized data requires additional integration. -In 1.2.x we started providing compile-time support for `kotlinx.serialization`. -From 2.0 we also provide `ArrowModule` for -[contextual serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#contextual-serialization). This is needed, among others, when the data is processed -by Ktor. +In Arrow 2.0 we have sprinkled some DSL dust over `Raise`, and now you can +write your code in a more linear way. Inside an `accumulate` block (or in +general, any `RaiseAccumulate`) you use `by accumulating` to execute some +computation keeping all the errors. + +```kotlin +// version with parZip +parZip( + { checkOneThing() }, + { checkOtherThing() } +) { a, b -> doSomething(a, b) } + +// version with accumulate +accumulate { + val a by accumulating { checkOneThing() } + val b by accumulating { checkOtherThing() } + doSomething(a, b) +} +``` + +This DSL also includes shortcuts for the most common operations, like +`bind`ing and accumulating any problem, or checking a single property +of some data. + +```kotlin +accumulate { + val name by Name(rawName).bindOrAccumulate() + val age by ensureOrAccumulate(rawAge >= 18) { UnderAge } + Person(name, age) +} +``` ## Additions to Fx @@ -45,7 +81,8 @@ parZip( The new `awaitAll` scope tries to improve the situation by tweaking the usual `async` mechanism, ensuring that all `Deferred` values are `await`ed once the first one is requested. That means that the previous code behaves -identically to the following. +identically to the following, that is, the call `file.await()` implicitly +awaits every `async` defined up to that point. ```kotlin awaitAll { @@ -107,9 +144,37 @@ brings an improved variant that requires no arguments if the type of the `Iterable` is known. Similar improvements have been applied to `.at` and `.index`. +## Pattern matching + One completely new feature in Arrow 2.0 is the _pattern matching_ DSL. By combining prisms and lenses one can specify a complex shape, and then check whether a value fits into that shape, extracting some pieces of information on the go. The DSL gets quite close to pattern -matching found in functional languages like Haskell or Scala. The -interested reader may check the documentation. +matching found in functional languages like Haskell or Scala. + +We do not intend this package to be a replacement for `when` expressions, +smart casts, and guards already provided by the language. On the other +hand, we acknowledge that pattern matching offers advantages when the +code needs to inspect very nested data. + +```kotlin +val User.name: String get() = this.matchOrThrow { + // Company(name = nm, director = Name(lastName = d)) + User.company(Company.name, Company.director(Name.lastName)) then { (nm, d) -> "$nm, att. $d" } + // Person(Name(firstName = fn), age if it < 18) + User.person(Person.name(Name.firstName), Person.age.suchThat { it < 18 }) then { (fn, _) -> fn } + // Person(Name(firstName = fn, lastName = ln)) -> "Sir/Madam $fn $ln" + User.person(Person.name(Name.firstName, Name.lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } +} +``` + +We also provide a new package, `arrow-match`, which provides the same +pattern matching DSL, but using Kotlin's reflection instead of optics. + +## Better support for kotlinx.serialization + +Using Arrow Core data types as part of serialized data requires additional integration. +In 1.2.x we started providing compile-time support for `kotlinx.serialization`. +From 2.0 on we also provide `ArrowModule` for +[contextual serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#contextual-serialization). This is needed, among others, when the data is processed +by Ktor. From de555b169a9824060724fb6ca6467d2c4f9d813d Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 3 Oct 2024 09:11:10 +0200 Subject: [PATCH 11/22] Add information about new retry --- content/docs/learn/resilience/retry-and-repeat.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/content/docs/learn/resilience/retry-and-repeat.md b/content/docs/learn/resilience/retry-and-repeat.md index 906e6f36..faeb922c 100644 --- a/content/docs/learn/resilience/retry-and-repeat.md +++ b/content/docs/learn/resilience/retry-and-repeat.md @@ -37,6 +37,18 @@ steps involved in using `Schedule`. It returns the last internal state of the scheduling policy or the error that happened running the action. +:::tip Retrying only on certain exceptions + +Since version 2.0, you can use specify a subclass of `Throwable` as first type argument +to `retry` to focus only on those exceptions. It is customary to leave the second type +argument unspecified. + +```kotlin +policy.retry { ... } +``` + +::: + ## Constructing a policy From d935b652d74f4c549fadc21b647e19dd45fd7252 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 3 Oct 2024 09:31:16 +0200 Subject: [PATCH 12/22] Links + Knit --- ...1-arrow-2-0.md => 2024-10-04-arrow-2-0.md} | 26 +++--- content/docs/learn/coroutines/stm.md | 8 +- .../docs/learn/resilience/retry-and-repeat.md | 2 +- .../test/kotlin/examples/example-stm-02.kt | 88 ------------------- .../test/kotlin/examples/example-stm-03.kt | 57 ++++++++---- .../test/kotlin/examples/example-stm-04.kt | 30 +++++++ .../src/test/kotlin/examples/test/STMTest.kt | 4 + 7 files changed, 92 insertions(+), 123 deletions(-) rename content/blog/{2024-07-01-arrow-2-0.md => 2024-10-04-arrow-2-0.md} (86%) create mode 100644 guide/src/test/kotlin/examples/example-stm-04.kt diff --git a/content/blog/2024-07-01-arrow-2-0.md b/content/blog/2024-10-04-arrow-2-0.md similarity index 86% rename from content/blog/2024-07-01-arrow-2-0.md rename to content/blog/2024-10-04-arrow-2-0.md index 74beec26..50fe25ac 100644 --- a/content/blog/2024-07-01-arrow-2-0.md +++ b/content/blog/2024-10-04-arrow-2-0.md @@ -53,7 +53,7 @@ accumulate { } ``` -This DSL also includes shortcuts for the most common operations, like +This DSL also includes shortcuts for the most common operations, like `bind`ing and accumulating any problem, or checking a single property of some data. @@ -78,7 +78,7 @@ parZip( ) { file, data -> Result(file, data) } ``` -The new `awaitAll` scope tries to improve the situation by tweaking the +The new [`awaitAll` scope](/learn/coroutines/parallel/#await-all-scopes) tries to improve the situation by tweaking the usual `async` mechanism, ensuring that all `Deferred` values are `await`ed once the first one is requested. That means that the previous code behaves identically to the following, that is, the call `file.await()` implicitly @@ -92,7 +92,7 @@ awaitAll { } ``` -We've also improved the STM block by allowing delegation as a means to +We've also improved the STM block by [allowing delegation](/learn/coroutines/stm/#reading-and-writing-concurrent-state) as a means to read or change the value of a `TVar`. ```kotlin @@ -105,9 +105,9 @@ fun STM.deposit(accVar: TVar, amount: Int): Unit { ## Clearer retries for particular exceptions -Until now, the `retry` operation in the Resilience module would capture +Until now, the [`retry` operation](/learn/resilience/retry-and-repeat/) in the Resilience module would capture any `Throwable` exception. From version 2.0 on you can specify a subclass -of `Throwable` to be the target for retrying, whereas the rest of +of `Throwable` to be the target for retrying, whereas the rest of exceptions will bubble as usual. ```kotlin @@ -117,14 +117,14 @@ Schedule.recurs(2) The subclass of exceptions must be given as a type argument. Alas, Kotlin does not allow giving only a subset of those, and `retry` -has two type parameters (the second one represents the output type of -the `Schedule`). Fortunately, you can ask the compiler to infer the +has two type parameters (the second one represents the output type of +the `Schedule`). Fortunately, you can ask the compiler to infer the second one using `_`. ## Improved optics The two **breaking changes** in Arrow 2.0 relate to optics. -First of all, the optics hierarchy has been greatly simplified: +First of all, the [optics hierarchy](/learn/immutable-data/intro/#many-optics-to-rule-them-all) has been greatly simplified: now we have traversals, optionals, lenses, prisms, and isos, and no more intermediate types. This smaller amount of types means that the type of optic compositions become easier to understand. @@ -138,7 +138,7 @@ In the 1.x series, a value of type `String?` would be presented as behavior you should apply `.notNull` after the optic corresponding to the field. -One pain point when building traversals was the need to provide an +One pain point when building [traversals](/learn/immutable-data/traversal/) was the need to provide an argument to `.every`, like `.every(Every.list())`. This new version brings an improved variant that requires no arguments if the type of the `Iterable` is known. Similar improvements have been applied @@ -146,10 +146,10 @@ to `.at` and `.index`. ## Pattern matching -One completely new feature in Arrow 2.0 is the _pattern matching_ DSL. +One completely new feature in Arrow 2.0 is the [_pattern matching_ DSL](/learn/immutable-data/matching/). By combining prisms and lenses one can specify a complex shape, and then check whether a value fits into that shape, extracting some -pieces of information on the go. The DSL gets quite close to pattern +pieces of information on the go. The DSL gets quite close to pattern matching found in functional languages like Haskell or Scala. We do not intend this package to be a replacement for `when` expressions, @@ -168,13 +168,13 @@ val User.name: String get() = this.matchOrThrow { } ``` -We also provide a new package, `arrow-match`, which provides the same +We also provide a new package, [`arrow-match`](/learn/immutable-data/matching/#matching-without-optics), which provides the same pattern matching DSL, but using Kotlin's reflection instead of optics. ## Better support for kotlinx.serialization Using Arrow Core data types as part of serialized data requires additional integration. -In 1.2.x we started providing compile-time support for `kotlinx.serialization`. +In 1.2.x we started providing compile-time [support for `kotlinx.serialization`](/learn/quickstart/serialization/#kotlinxserialization). From 2.0 on we also provide `ArrowModule` for [contextual serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#contextual-serialization). This is needed, among others, when the data is processed by Ktor. diff --git a/content/docs/learn/coroutines/stm.md b/content/docs/learn/coroutines/stm.md index e7656679..24f5c8af 100644 --- a/content/docs/learn/coroutines/stm.md +++ b/content/docs/learn/coroutines/stm.md @@ -98,7 +98,7 @@ they become implicit in the syntax. import arrow.fx.stm.atomically import arrow.fx.stm.TVar import arrow.fx.stm.STM ----> +--> ```kotlin fun STM.deposit(accVar: TVar, amount: Int): Unit { @@ -111,7 +111,7 @@ fun STM.deposit(accVar: TVar, amount: Int): Unit { +--> @@ -196,7 +196,7 @@ suspend fun example() = coroutineScope { acc2.unsafeRead() shouldBe 350 } ``` - + `retry` can be used to implement a lot of complex transactions, @@ -249,7 +249,7 @@ suspend fun example() { atomically { transaction(v) } shouldBe 5 } ``` - + ## Exceptions diff --git a/content/docs/learn/resilience/retry-and-repeat.md b/content/docs/learn/resilience/retry-and-repeat.md index faeb922c..d52b33ae 100644 --- a/content/docs/learn/resilience/retry-and-repeat.md +++ b/content/docs/learn/resilience/retry-and-repeat.md @@ -43,7 +43,7 @@ Since version 2.0, you can use specify a subclass of `Throwable` as first type a to `retry` to focus only on those exceptions. It is customary to leave the second type argument unspecified. -```kotlin +``` policy.retry { ... } ``` diff --git a/guide/src/test/kotlin/examples/example-stm-02.kt b/guide/src/test/kotlin/examples/example-stm-02.kt index 305acfc6..9f2ab0e3 100644 --- a/guide/src/test/kotlin/examples/example-stm-02.kt +++ b/guide/src/test/kotlin/examples/example-stm-02.kt @@ -8,99 +8,11 @@ import io.kotest.matchers.shouldBe import arrow.fx.stm.atomically import arrow.fx.stm.TVar import arrow.fx.stm.STM ----> -```kotlin fun STM.deposit(accVar: TVar, amount: Int): Unit { val acc by accVar // property delegation val current = acc // implicit 'read' acc = current + amount // implicit 'write' // or simply, acc = acc + amount } -``` - - - - - -::: - -### Other STM data structures - -The following types are built upon `TVar`s and provided out of the box with Arrow: - -- [`TQueue`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-queue/index.html): transactional mutable queue, -- [`TMVar`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-m-var/index.html): mutable transactional variable that may be empty, -- [`TSet`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-set/index.html), [`TMap`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-map/index.html): transactional `Set` and `Map`, -- [`TArray`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-array/index.html): array of `TVar`s, -- [`TSemaphore`](https://apidocs.arrow-kt.io/arrow-fx-stm/arrow.fx.stm/-t-semaphore/index.html): transactional semaphore. - -:::tip - -Note that, in most cases, using these data structures is much more efficient than -wrapping their "regular" version in a `TVar`. For example, a `TSet
` performs -better than a `TVar>` because the latter needs to "lock" the entire set -on modification, whereas the former knows that only the affected entries need -to be taken into account. - -::: - -## Retries - -It is sometimes beneficial to manually abort the current transaction if an -invalid state has been reached. For example, a `TQueue` had no elements to read. -The aborted transaction will automatically restart once any previously accessed -variable has changed. - -Here in this example, we've changed `withdraw` to use `retry` and thus wait until -enough money is in the account, which after a few seconds happens to be the case. - - ```kotlin fun one(): Either = Either.Right(1) -val old: Either> = - listOf(1, 2, 3).traverse { one() } +// val old: Either> = +// listOf(1, 2, 3).traverse { one() } val new: Either> = either { listOf(1, 2, 3).map { one().bind() } @@ -170,7 +170,7 @@ import arrow.core.raise.either ```kotlin fun one(): Either = Either.Right(1) -val old: Either = one().zip(one()) { x, y -> x + y } +// val old: Either = one().zip(one()) { x, y -> x + y } val new: Either = either { one().bind() + one().bind() } @@ -280,29 +280,33 @@ some deprecated methods may need to add an extra manual step, besides the automa The replacement of deprecated `foldMap` for `Iterable`, `Option` and `Either` requires to replace the `Monoid` parameter with an `empty` value of the type contained in the removed `Monoid`. Let's see this in action: + ```kotlin fun booleanToString(b: Boolean): String = if (b) "IS TRUE! :)" else "IS FALSE.... :(" +``` +``` fun deprecatedFoldMap() { val e1: Either = false.right() e1.foldMap(Monoid.string(), ::booleanToString) shouldBe "IS FALSE.... :(" } ``` + ``` // Executing automatic replacement fun migrateFoldMap() { @@ -311,6 +315,7 @@ fun migrateFoldMap() { } ``` + ```kotlin // Adding the empty value to complete the replacement of the deprecated method fun migrateFoldMap() { @@ -320,10 +325,11 @@ fun migrateFoldMap() { ``` ### combine + All deprecated `combine` methods are suggested to be replaced by the lambda `{a, b -> a + b}`, which will cover almost all possible replacements successfully. One of the cases that will need some manual fix is the following: -```kotlin +``` fun deprecatedZip() { val nullableLongMonoid = object : Monoid { override fun empty(): Long? = 0 @@ -336,7 +342,9 @@ fun deprecatedZip() { res shouldBe Validated.Valid(3) } ``` + When we replace the deprecated `zip` method: + ``` // Executing automatic replacement fun migrateZip(){ @@ -348,8 +356,10 @@ fun migrateZip(){ ) { a, _ -> a }.toValidated() } ``` + In this case, we do not have the `+` operation for `Long?`, so we need to add it manually: -```kotlin + +``` fun migrateZip() { val validated: Validated = 3.valid() val res = Either.zipOrAccumulate( @@ -362,10 +372,12 @@ fun migrateZip() { ``` ### combineAll + In a similar situation like [foldMap](#foldmap), the replacement of deprecated `combineAll` for `Iterable`, `Option` and `Validate` needs to add manually the `initial` parameter, in the replacement with `fold` method. Let's do a replacement to see how to achieve this: -```kotlin + +``` fun deprecatedCombineAll() { val l: List = listOf(1, 2, 3, 4, 5) l.combineAll(Monoid.int()) shouldBe 10 @@ -389,10 +401,12 @@ fun migrateCombineAll() { ``` ### replicate + `replicate` also needs a bit of *help* when removing the deprecated `Monoid` for `Option` and `Either`. Again, `fold` is the recommended replacement method, so we'll need to provide the `initial` parameter in the `fold`. Let's see this with an `Either`: -```kotlin + +``` fun deprecatedReplicate() { val rEither: Either = 125.right() val n = 3 @@ -424,23 +438,27 @@ fun migrateReplicate() { ``` ## Ior + Most of the `Ior` data type deprecated method migrations related to `traverse` and `crosswalk`, must be replaced manually. The main reason is that `Intellij` does not know how to infer some types when we're using generics. Although this situation can be a bit annoying, this is a good excuse for the user to navigate and get more expertise on the `Arrow` source code. Let's see a few examples to be more familiar with these special cases: ### crosswalk + Given the `Ior` implementation of `crosswalk`: -``` -public inline fun crosswalk(fa: (B) -> Iterable): List> = + +```kotlin +public inline fun Ior.crosswalk(fa: (B) -> Iterable): List> = fold( { emptyList() }, - { b -> fa(b).map { Right(it) } }, - { a, b -> fa(b).map { Both(a, it) } } + { b -> fa(b).map { Ior.Right(it) } }, + { a, b -> fa(b).map { Ior.Both(a, it) } } ) ``` And an example that use `crosswalk`: + ```kotlin fun deprecatedCrosswalk() { val rightIor: Ior = Ior.Right(124) @@ -451,6 +469,7 @@ fun deprecatedCrosswalk() { The result of replacing manually the `crosswalk` call using the `fold` implementation would be: + ### traverse + In a similar situation we have the `Ior` `traverse` method for a function that returns an `Option`. Given the implementation of `traverse`: -``` -public inline fun traverse(fa: (B) -> Option): Option> { - return fold( - { a -> Some(Left(a)) }, - { b -> fa(b).map { Right(it) } }, - { a, b -> fa(b).map { Both(a, it) } } - ) - } -``` -And an example that use `traverse`: + +```kotlin +public inline fun Ior.traverse(fa: (B) -> Option): Option> { + return fold( + { a -> Some(Ior.Left(a)) }, + { b -> fa(b).map { Ior.Right(it) } }, + { a, b -> fa(b).map { Ior.Both(a, it) } } + ) + } +``` + +And an example that use `traverse`: + ```kotlin fun evenOpt(i: Int): Option = if(i % 2 == 0) i.some() else None diff --git a/content/docs/learn/typed-errors/working-with-typed-errors.md b/content/docs/learn/typed-errors/working-with-typed-errors.md index 6f8a058a..04bdbcd6 100644 --- a/content/docs/learn/typed-errors/working-with-typed-errors.md +++ b/content/docs/learn/typed-errors/working-with-typed-errors.md @@ -194,8 +194,8 @@ fun example() { -Without context receivers, these functions look pretty different depending on if we use `Raise` or `Either`. This is because we sacrifice our _extension receiver_ for `Raise`. -And thus, the `Raise` based computation cannot be an extension function on `User`. With context receivers, we could've defined it as: +Alas, these functions look pretty different depending on if we use `Raise` or `Either`. This is because we sacrifice our _extension receiver_ for `Raise`. +And thus, the `Raise` based computation cannot be an extension function on `User`. In the future, it will be possible to define a better version using context parameters. -```kotlin -context(Raise) +``` +context(_: Raise) fun User.isValid(): Unit = ensure(id > 0) { UserNotFound("User without a valid id: $id") } ``` @@ -821,11 +821,14 @@ data class User private constructor(val name: String, val age: Int) { With this change, the problems are correctly accumulated. Now we can present the user all the problems in the form at once. -```kotlin -fun example() { +``` +fun sample() { User("", -1) shouldBe Left(nonEmptyListOf(UserProblem.EmptyName, UserProblem.NegativeAge(-1))) } ``` + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 29f9bffa..af2f3729 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ coroutines = "1.9.0" kotest = "5.9.1" kotlin = "2.0.20" knit = "0.5.0" -arrow = "1.2.4" +arrow = "2.0.0-alpha.4" ksp = "2.0.20-1.0.25" suspendapp = "0.4.0" kotlinKafka = "0.4.0" @@ -30,6 +30,7 @@ arrow-optics-reflect = { module = "io.arrow-kt:arrow-optics-reflect", version.re arrow-optics-compose = { module = "io.arrow-kt:arrow-optics-compose", version.ref = "arrow" } arrow-collectors = { module = "io.arrow-kt:arrow-collectors", version.ref = "arrow" } arrow-eval = { module = "io.arrow-kt:arrow-eval", version.ref = "arrow" } +arrow-functions = { module = "io.arrow-kt:arrow-functions", version.ref = "arrow" } arrow-cache4k = { module = "io.arrow-kt:arrow-cache4k", version.ref = "arrow" } kotlinx-knit = { module = "org.jetbrains.kotlinx:kotlinx-knit", version.ref = "knit" } kotlinx-knit-test = { module = "org.jetbrains.kotlinx:kotlinx-knit-test", version.ref = "knit" } diff --git a/guide/build.gradle.kts b/guide/build.gradle.kts index 8515cf20..3cddacb2 100644 --- a/guide/build.gradle.kts +++ b/guide/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { testImplementation(libs.coroutines.test) testImplementation(libs.kotlinx.knit.test) testImplementation(libs.arrow.core.lib) - // testImplementation(libs.arrow.core.highArity) + testImplementation(libs.arrow.core.highArity) testImplementation(libs.arrow.core.serialization) testImplementation(libs.arrow.fx.coroutines) testImplementation(libs.arrow.autoclose) @@ -29,6 +29,7 @@ dependencies { // testImplementation(libs.arrow.optics.compose) testImplementation(libs.arrow.collectors) testImplementation(libs.arrow.eval) + testImplementation(libs.arrow.functions) testImplementation(libs.arrow.cache4k) testImplementation(libs.kotest.assertions.core) testImplementation(libs.kotest.property) @@ -49,5 +50,6 @@ tasks { withType().configureEach { compilerOptions.freeCompilerArgs.add("-Xcontext-receivers") + compilerOptions.freeCompilerArgs.add("-Xconsistent-data-class-copy-visibility") } } diff --git a/guide/src/test/kotlin/examples/example-migration-guide-01.kt b/guide/src/test/kotlin/examples/example-migration-guide-01.kt index b0fa13c7..dcfd32d3 100644 --- a/guide/src/test/kotlin/examples/example-migration-guide-01.kt +++ b/guide/src/test/kotlin/examples/example-migration-guide-01.kt @@ -2,13 +2,13 @@ package arrow.website.examples.exampleMigrationGuide01 import arrow.core.Either -import arrow.core.traverse +// import arrow.core.traverse import arrow.core.raise.either fun one(): Either = Either.Right(1) -val old: Either> = - listOf(1, 2, 3).traverse { one() } +// val old: Either> = +// listOf(1, 2, 3).traverse { one() } val new: Either> = either { listOf(1, 2, 3).map { one().bind() } diff --git a/guide/src/test/kotlin/examples/example-migration-guide-02.kt b/guide/src/test/kotlin/examples/example-migration-guide-02.kt index 46ca2b88..13de5b18 100644 --- a/guide/src/test/kotlin/examples/example-migration-guide-02.kt +++ b/guide/src/test/kotlin/examples/example-migration-guide-02.kt @@ -7,7 +7,7 @@ import arrow.core.raise.either fun one(): Either = Either.Right(1) -val old: Either = one().zip(one()) { x, y -> x + y } +// val old: Either = one().zip(one()) { x, y -> x + y } val new: Either = either { one().bind() + one().bind() } diff --git a/guide/src/test/kotlin/examples/example-migration-guide-05.kt b/guide/src/test/kotlin/examples/example-migration-guide-05.kt index 036dcdd2..67cfadbe 100644 --- a/guide/src/test/kotlin/examples/example-migration-guide-05.kt +++ b/guide/src/test/kotlin/examples/example-migration-guide-05.kt @@ -2,70 +2,32 @@ package arrow.website.examples.exampleMigrationGuide05 import arrow.core.Either -import arrow.core.replicate -import arrow.core.Valid -import arrow.core.Validated -import arrow.core.combineAll +// import arrow.core.replicate +// import arrow.core.Valid +// import arrow.core.Validated +// import arrow.core.combineAll import arrow.core.Ior import arrow.core.raise.nullable import arrow.core.right -import arrow.core.valid +// import arrow.core.valid import arrow.core.zip -import arrow.typeclasses.Monoid +// import arrow.typeclasses.Monoid import io.kotest.matchers.shouldBe fun booleanToString(b: Boolean): String = if (b) "IS TRUE! :)" else "IS FALSE.... :(" -fun deprecatedFoldMap() { - val e1: Either = false.right() - e1.foldMap(Monoid.string(), ::booleanToString) shouldBe "IS FALSE.... :(" -} - // Adding the empty value to complete the replacement of the deprecated method fun migrateFoldMap() { val e1: Either = false.right() e1.fold({""}, ::booleanToString) shouldBe "IS FALSE.... :(" } -fun deprecatedZip() { - val nullableLongMonoid = object : Monoid { - override fun empty(): Long? = 0 - override fun Long?.combine(b: Long?): Long? = - nullable { this@combine.bind() + b.bind() } - } - - val validated: Validated = 3.valid() - val res = validated.zip(nullableLongMonoid, Valid(Unit)) { a, _ -> a } // zip and Monoid are deprecated - res shouldBe Validated.Valid(3) -} - -fun migrateZip() { - val validated: Validated = 3.valid() - val res = Either.zipOrAccumulate( - { e1, e2 -> nullable { e1.bind() + e2.bind() } }, - validated.toEither(), - Valid(Unit).toEither() - ) { a, _ -> a }.toValidated() - res shouldBe Validated.Valid(3) -} - -fun deprecatedCombineAll() { - val l: List = listOf(1, 2, 3, 4, 5) - l.combineAll(Monoid.int()) shouldBe 10 -} - // Adding the initial value to complete the replacement of the deprecated method fun migrateCombineAll() { val l: List = listOf(1, 2, 3, 4, 5) l.fold(0) { a1, a2 -> a1 + a2 } shouldBe 10 } -fun deprecatedReplicate() { - val rEither: Either = 125.right() - val n = 3 - rEither.replicate(n, Monoid.int()) shouldBe Either.Right(375) -} - // Adding the empty value to complete the replacement of the deprecated method fun migrateReplicate() { val rEither: Either = 125.right() @@ -76,6 +38,13 @@ fun migrateReplicate() { res shouldBe Either.Right(375) } +public inline fun Ior.crosswalk(fa: (B) -> Iterable): List> = + fold( + { emptyList() }, + { b -> fa(b).map { Ior.Right(it) } }, + { a, b -> fa(b).map { Ior.Both(a, it) } } + ) + fun deprecatedCrosswalk() { val rightIor: Ior = Ior.Right(124) val result = rightIor.crosswalk { listOf(it) } diff --git a/guide/src/test/kotlin/examples/example-migration-guide-07.kt b/guide/src/test/kotlin/examples/example-migration-guide-07.kt index 485132fa..5e270ca4 100644 --- a/guide/src/test/kotlin/examples/example-migration-guide-07.kt +++ b/guide/src/test/kotlin/examples/example-migration-guide-07.kt @@ -8,6 +8,14 @@ import arrow.core.Some import arrow.core.some import io.kotest.matchers.shouldBe +public inline fun Ior.traverse(fa: (B) -> Option): Option> { + return fold( + { a -> Some(Ior.Left(a)) }, + { b -> fa(b).map { Ior.Right(it) } }, + { a, b -> fa(b).map { Ior.Both(a, it) } } + ) + } + fun evenOpt(i: Int): Option = if(i % 2 == 0) i.some() else None fun deprecatedTraverse() { diff --git a/guide/src/test/kotlin/examples/example-typed-errors-04.kt b/guide/src/test/kotlin/examples/example-typed-errors-04.kt index 571ed47b..317debe1 100644 --- a/guide/src/test/kotlin/examples/example-typed-errors-04.kt +++ b/guide/src/test/kotlin/examples/example-typed-errors-04.kt @@ -6,7 +6,3 @@ import arrow.core.raise.ensure data class User(val id: Long) data class UserNotFound(val message: String) - -context(Raise) -fun User.isValid(): Unit = - ensure(id > 0) { UserNotFound("User without a valid id: $id") } diff --git a/guide/src/test/kotlin/examples/example-typed-errors-20.kt b/guide/src/test/kotlin/examples/example-typed-errors-20.kt index fb713e26..d215d2cd 100644 --- a/guide/src/test/kotlin/examples/example-typed-errors-20.kt +++ b/guide/src/test/kotlin/examples/example-typed-errors-20.kt @@ -25,7 +25,4 @@ data class User private constructor(val name: String, val age: Int) { } } } - -fun example() { - User("", -1) shouldBe Left(nonEmptyListOf(UserProblem.EmptyName, UserProblem.NegativeAge(-1))) -} +fun example() { } From fde09805806e6c620c1e921bfaa0abd7a5deacaa Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 3 Oct 2024 23:17:40 +0200 Subject: [PATCH 14/22] Small fix to STM code --- content/docs/learn/coroutines/stm.md | 2 +- guide/src/test/kotlin/examples/example-stm-02.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/learn/coroutines/stm.md b/content/docs/learn/coroutines/stm.md index 24f5c8af..7b84dbc0 100644 --- a/content/docs/learn/coroutines/stm.md +++ b/content/docs/learn/coroutines/stm.md @@ -102,7 +102,7 @@ import arrow.fx.stm.STM ```kotlin fun STM.deposit(accVar: TVar, amount: Int): Unit { - val acc by accVar // property delegation + var acc by accVar // property delegation val current = acc // implicit 'read' acc = current + amount // implicit 'write' // or simply, acc = acc + amount diff --git a/guide/src/test/kotlin/examples/example-stm-02.kt b/guide/src/test/kotlin/examples/example-stm-02.kt index 9f2ab0e3..69769a71 100644 --- a/guide/src/test/kotlin/examples/example-stm-02.kt +++ b/guide/src/test/kotlin/examples/example-stm-02.kt @@ -10,7 +10,7 @@ import arrow.fx.stm.TVar import arrow.fx.stm.STM fun STM.deposit(accVar: TVar, amount: Int): Unit { - val acc by accVar // property delegation + var acc by accVar // property delegation val current = acc // implicit 'read' acc = current + amount // implicit 'write' // or simply, acc = acc + amount From b9fc0e6f76e7a9d3db24b7ea9431adb478732683 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 4 Oct 2024 07:25:35 +0200 Subject: [PATCH 15/22] Use Knit in matching examples --- content/docs/learn/immutable-data/matching.md | 31 ++++++++++++++----- gradle/libs.versions.toml | 1 + guide/build.gradle.kts | 1 + .../kotlin/examples/example-matching-01.kt | 31 +++++++++++++++++++ .../kotlin/examples/example-matching-02.kt | 25 +++++++++++++++ 5 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 guide/src/test/kotlin/examples/example-matching-01.kt create mode 100644 guide/src/test/kotlin/examples/example-matching-02.kt diff --git a/content/docs/learn/immutable-data/matching.md b/content/docs/learn/immutable-data/matching.md index de1ddedc..dde233d0 100644 --- a/content/docs/learn/immutable-data/matching.md +++ b/content/docs/learn/immutable-data/matching.md @@ -12,8 +12,7 @@ and with Arrow Optics those same ideas translate to Kotlin. - @@ -33,7 +32,7 @@ and data classes for which we get lenses. ): User { companion object } @optics data class Company( val name: String, val director: Name, val address: String -): User +): User { companion object } ``` Here is the implementation of a small function that returns how to show @@ -46,7 +45,7 @@ val User.name: String get() = this.matchOrThrow { // Company(name = nm, director = Name(lastName = d)) User.company(Company.name, Company.director(Name.lastName)) then { (nm, d) -> "$nm, att. $d" } // Person(Name(firstName = fn), age if it < 18) - User.person(Person.name(Name.firstName), Person.age.suchThat { it < 18 }) then { (fn, _) -> fn } + User.person(Person.name(Name.firstName), Person.age.takeIf { it < 18 }) then { (fn, _) -> fn } // Person(Name(firstName = fn, lastName = ln)) User.person(Person.name(Name.firstName, Name.lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } } @@ -86,7 +85,7 @@ names in the body. The pattern matching mechanism in `arrow-optics` also allow you to add checks about values at any point in the pattern. In our example above, the `age` is checked to decide whether to -apply the first match, using `.suchThat { it < 18 }`. +apply the first match, using `.takeIf { it < 18 }`. :::tip It @@ -111,6 +110,7 @@ fun User.directorOrNull(): Name? = this.matchOrThrow { default { null } } ``` + :::warning Exhaustiveness checking @@ -144,16 +144,33 @@ Since we do not have optics, we need another way to represent types and property You can see those changes in action in the "translation" of the first example in this section to `arrow-match`. + + ```kotlin val User.name: String get() = this.matchOrThrow { // Company(name = nm, director = Name(lastName = d)) Company::class.of(Company::name, Company::director.of(Name::lastName)) then { (nm, d) -> "$nm, att. $d" } // Person(Name(firstName = fn), age if it < 18) - Person::class.of(Person::name.of(Name::firstName), Person::age.suchThat { it < 18 }) then { (fn, _) -> fn } + Person::class.of(Person::name.of(Name::firstName), Person::age.takeIf { it < 18 }) then { (fn, _) -> fn } // Person(Name(firstName = fn, lastName = ln)) Person::class.of(Person::name.of(Name::firstName, Name::lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } } ``` + ## Matching with guards @@ -162,7 +179,7 @@ In most cases patterns can be recreated using `when` expressions with guards and property access, leading to more idiomatic code. This approach has the additional benefit of the exhaustiveness checking performed by the compiler. -```kotlin +``` val User.name: String get() = when (this) { is Company -> "$name, att ${director.lastName}" is Person if age < 18 -> name.firstName diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af2f3729..158df961 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ arrow-collectors = { module = "io.arrow-kt:arrow-collectors", version.ref = "arr arrow-eval = { module = "io.arrow-kt:arrow-eval", version.ref = "arrow" } arrow-functions = { module = "io.arrow-kt:arrow-functions", version.ref = "arrow" } arrow-cache4k = { module = "io.arrow-kt:arrow-cache4k", version.ref = "arrow" } +arrow-match = { module = "io.arrow-kt:arrow-match", version.ref = "arrow" } kotlinx-knit = { module = "org.jetbrains.kotlinx:kotlinx-knit", version.ref = "knit" } kotlinx-knit-test = { module = "org.jetbrains.kotlinx:kotlinx-knit-test", version.ref = "knit" } suspendapp = { module = "io.arrow-kt:suspendapp", version.ref = "suspendapp" } diff --git a/guide/build.gradle.kts b/guide/build.gradle.kts index 3cddacb2..24efffea 100644 --- a/guide/build.gradle.kts +++ b/guide/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { testImplementation(libs.arrow.eval) testImplementation(libs.arrow.functions) testImplementation(libs.arrow.cache4k) + testImplementation(libs.arrow.match) testImplementation(libs.kotest.assertions.core) testImplementation(libs.kotest.property) testImplementation(libs.suspendapp) diff --git a/guide/src/test/kotlin/examples/example-matching-01.kt b/guide/src/test/kotlin/examples/example-matching-01.kt new file mode 100644 index 00000000..340a3f4f --- /dev/null +++ b/guide/src/test/kotlin/examples/example-matching-01.kt @@ -0,0 +1,31 @@ +// This file was automatically generated from matching.md by Knit tool. Do not edit. +package arrow.website.examples.exampleMatching01 + +import arrow.optics.* +import arrow.optics.match.* + +@optics data class Name( + val firstName: String, val lastName: String +) { companion object } + +@optics sealed interface User { companion object } +@optics data class Person( + val name: Name, val age: Int +): User { companion object } +@optics data class Company( + val name: String, val director: Name, val address: String +): User { companion object } + +val User.name: String get() = this.matchOrThrow { + // Company(name = nm, director = Name(lastName = d)) + User.company(Company.name, Company.director(Name.lastName)) then { (nm, d) -> "$nm, att. $d" } + // Person(Name(firstName = fn), age if it < 18) + User.person(Person.name(Name.firstName), Person.age.takeIf { it < 18 }) then { (fn, _) -> fn } + // Person(Name(firstName = fn, lastName = ln)) + User.person(Person.name(Name.firstName, Name.lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } +} + +fun User.directorOrNull(): Name? = this.matchOrThrow { + User.company(Company.director) then { it } + default { null } +} diff --git a/guide/src/test/kotlin/examples/example-matching-02.kt b/guide/src/test/kotlin/examples/example-matching-02.kt new file mode 100644 index 00000000..67b00134 --- /dev/null +++ b/guide/src/test/kotlin/examples/example-matching-02.kt @@ -0,0 +1,25 @@ +// This file was automatically generated from matching.md by Knit tool. Do not edit. +package arrow.website.examples.exampleMatching02 + +import arrow.match.* + +data class Name( + val firstName: String, val lastName: String +) + +sealed interface User +data class Person( + val name: Name, val age: Int +): User +data class Company( + val name: String, val director: Name, val address: String +): User + +val User.name: String get() = this.matchOrThrow { + // Company(name = nm, director = Name(lastName = d)) + Company::class.of(Company::name, Company::director.of(Name::lastName)) then { (nm, d) -> "$nm, att. $d" } + // Person(Name(firstName = fn), age if it < 18) + Person::class.of(Person::name.of(Name::firstName), Person::age.takeIf { it < 18 }) then { (fn, _) -> fn } + // Person(Name(firstName = fn, lastName = ln)) + Person::class.of(Person::name.of(Name::firstName, Name::lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } +} From 71cc9f07cd2120cac230feace8778c20dfac1c8e Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Sat, 5 Oct 2024 22:36:41 +0200 Subject: [PATCH 16/22] Accumulation --- content/blog/2024-10-04-arrow-2-0.md | 10 +-- .../typed-errors/working-with-typed-errors.md | 62 +++++++++++++++++-- .../examples/example-typed-errors-20.kt | 4 ++ .../examples/example-typed-errors-21.kt | 29 ++++++--- .../examples/example-typed-errors-22.kt | 18 ++++++ .../kotlin/examples/test/TypedErrorsTest.kt | 4 ++ 6 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 guide/src/test/kotlin/examples/example-typed-errors-22.kt diff --git a/content/blog/2024-10-04-arrow-2-0.md b/content/blog/2024-10-04-arrow-2-0.md index 50fe25ac..cddf2264 100644 --- a/content/blog/2024-10-04-arrow-2-0.md +++ b/content/blog/2024-10-04-arrow-2-0.md @@ -19,17 +19,17 @@ Apart from stabilization and general bug fixing, the theme of this release is improving the different DSLs provided by Arrow libraries. Our goal is to empower developers to write more succinct and readable code. -* [Simple accumulation in Raise](#simple-accumulation-in-raise) +* [Simple accumulation in Raise](#simple-accumulation-in-raise) (experimental) * [Additions to Fx](#additions-to-fx) * [Clearer retries for particular exceptions](#clearer-retries-for-particular-exceptions) -* [Improved optics](#improved-optics) +* [Improved optics](#improved-optics) (breaking changes) * [Pattern matching](#pattern-matching) * [Better support for kotlinx.serialization](#better-support-for-kotlinxserialization) ## Simple accumulation in Raise One of the core concepts when working with typed errors is the distinction -between fail-first and accumulation of errors. Until now, the latter mode +between fail-first and [accumulation of errors](/learn/typed-errors/working-with-typed-errors/#accumulating-different-computations). Until now, the latter mode required using `parZip` and `parMap`, which sometimes obscure the actual flow of the computation. @@ -60,11 +60,13 @@ of some data. ```kotlin accumulate { val name by Name(rawName).bindOrAccumulate() - val age by ensureOrAccumulate(rawAge >= 18) { UnderAge } + ensureOrAccumulate(age >= 18) { UnderAge } Person(name, age) } ``` +Note that the API may still undergo some change. At this point you need `@OptIn(ExperimentalRaiseAccumulateApi::class)` to allow their usage in your code. + ## Additions to Fx Writing coroutine-heavy code may become cumbersome over time, especially if diff --git a/content/docs/learn/typed-errors/working-with-typed-errors.md b/content/docs/learn/typed-errors/working-with-typed-errors.md index 04bdbcd6..c0b6c8f3 100644 --- a/content/docs/learn/typed-errors/working-with-typed-errors.md +++ b/content/docs/learn/typed-errors/working-with-typed-errors.md @@ -737,6 +737,7 @@ fun example() = either { In the example above we are providing one single function to operate on a sequence of elements. Another important and related scenario is accumulating different errors, but each of them coming from different computations. For example, you need to perform validation over the different fields of a form, and accumulate the errors, but each field has different constraints. +Arrow supports two different styles for this task: using `zipOrAccumulate`, and using the `accumulate` scope. As a guiding example, let's consider information about a user, where the name shouldn't be empty and the age should be non-negative. @@ -803,8 +804,12 @@ sealed interface UserProblem { --> If you want to gather as many validation problems as possible, you need to switch to _accumulation_, as done above with `mapOrAccumulate`. -When each of the validations is different, you should reach to `zipOrAccumulate`: each of the arguments defines one independent validation, -and the final block defines what to do when all the validations were successful, that is, when no problem was `raise`d during execution. +Bu in this case we want to run independent validations of a different type, each for each field comprising the value. + +The first approach is to use `zipOrAccumulate`. +In that case the first arguments define the different independent validations, often as a block of code. +If all those validations succeed, that is, when no problem was `raise`d during execution of any of them, +then the final block is executed. The result of the independent validations are made available, in case they are required. ```kotlin data class User private constructor(val name: String, val age: Int) { @@ -821,7 +826,7 @@ data class User private constructor(val name: String, val age: Int) { With this change, the problems are correctly accumulated. Now we can present the user all the problems in the form at once. -``` +```kotlin fun sample() { User("", -1) shouldBe Left(nonEmptyListOf(UserProblem.EmptyName, UserProblem.NegativeAge(-1))) } @@ -832,6 +837,55 @@ fun example() { } +The second approach involves delimiting a scope where accumulation should take place using `accumulate`. That way we bring into scope variations of most functions described above, like `ensureOrAccumulate` and `bindOrAccumulate`. One important difference, though, is that when the computation returns a value, you must use `by` (property delegation) instead of `=` to obtain the value. + +``` +accumulate { + val thing by checkThing().bindOrAccumulate() +} +``` + +Translating the example above to this new style leads to the following code. We introduce no delegation because `ensureOrAccumulate` returns no interesting value. + + + +```kotlin +data class User private constructor(val name: String, val age: Int) { + companion object { + operator fun invoke(name: String, age: Int): Either, User> = either { + accumulate { + ensureOrAccumulate(name.isNotEmpty()) { UserProblem.EmptyName } + ensureOrAccumulate(age >= 0) { UserProblem.NegativeAge(age) } + User(name, age) + } + } + } +} +``` + +The behavior is exactly the same as with `zipOrAccumulate`, that is, all potential errors are accumulated. + +```kotlin +fun example() { + User("", -1) shouldBe Left(nonEmptyListOf(UserProblem.EmptyName, UserProblem.NegativeAge(-1))) +} +``` + + + :::tip Error accumulation and concurrency In addition to accumulating errors, you may want to perform each of the tasks within `zipOrAccumulate` or `mapOrAccumulate` in parallel. @@ -866,7 +920,7 @@ fun example() { intError shouldBe Either.Left("problem".length) } --> - + A very common pattern is using `withError` to "bridge" validation errors of sub-components into validation errors of the larger value. diff --git a/guide/src/test/kotlin/examples/example-typed-errors-20.kt b/guide/src/test/kotlin/examples/example-typed-errors-20.kt index d215d2cd..47aa49bf 100644 --- a/guide/src/test/kotlin/examples/example-typed-errors-20.kt +++ b/guide/src/test/kotlin/examples/example-typed-errors-20.kt @@ -25,4 +25,8 @@ data class User private constructor(val name: String, val age: Int) { } } } + +fun sample() { + User("", -1) shouldBe Left(nonEmptyListOf(UserProblem.EmptyName, UserProblem.NegativeAge(-1))) +} fun example() { } diff --git a/guide/src/test/kotlin/examples/example-typed-errors-21.kt b/guide/src/test/kotlin/examples/example-typed-errors-21.kt index 4f4cdcec..7f7c5d9a 100644 --- a/guide/src/test/kotlin/examples/example-typed-errors-21.kt +++ b/guide/src/test/kotlin/examples/example-typed-errors-21.kt @@ -1,18 +1,31 @@ // This file was automatically generated from working-with-typed-errors.md by Knit tool. Do not edit. package arrow.website.examples.exampleTypedErrors21 -import arrow.core.* -import arrow.core.raise.* +import arrow.core.Either +import arrow.core.Either.Left +import arrow.core.NonEmptyList +import arrow.core.nonEmptyListOf +import arrow.core.raise.either +import arrow.core.raise.accumulate import io.kotest.matchers.shouldBe -val stringError: Either = "problem".left() +sealed interface UserProblem { + object EmptyName: UserProblem + data class NegativeAge(val age: Int): UserProblem +} -val intError: Either = either { - // transform error String -> Int - withError({ it.length }) { - stringError.bind() +data class User private constructor(val name: String, val age: Int) { + companion object { + operator fun invoke(name: String, age: Int): Either, User> = either { + accumulate { + ensureOrAccumulate(name.isNotEmpty()) { UserProblem.EmptyName } + ensureOrAccumulate(age >= 0) { UserProblem.NegativeAge(age) } + User(name, age) + } + } } } + fun example() { - intError shouldBe Either.Left("problem".length) + User("", -1) shouldBe Left(nonEmptyListOf(UserProblem.EmptyName, UserProblem.NegativeAge(-1))) } diff --git a/guide/src/test/kotlin/examples/example-typed-errors-22.kt b/guide/src/test/kotlin/examples/example-typed-errors-22.kt new file mode 100644 index 00000000..cdd3b7a1 --- /dev/null +++ b/guide/src/test/kotlin/examples/example-typed-errors-22.kt @@ -0,0 +1,18 @@ +// This file was automatically generated from working-with-typed-errors.md by Knit tool. Do not edit. +package arrow.website.examples.exampleTypedErrors22 + +import arrow.core.* +import arrow.core.raise.* +import io.kotest.matchers.shouldBe + +val stringError: Either = "problem".left() + +val intError: Either = either { + // transform error String -> Int + withError({ it.length }) { + stringError.bind() + } +} +fun example() { + intError shouldBe Either.Left("problem".length) +} diff --git a/guide/src/test/kotlin/examples/test/TypedErrorsTest.kt b/guide/src/test/kotlin/examples/test/TypedErrorsTest.kt index 9c0de5b0..91b6e9bb 100644 --- a/guide/src/test/kotlin/examples/test/TypedErrorsTest.kt +++ b/guide/src/test/kotlin/examples/test/TypedErrorsTest.kt @@ -55,4 +55,8 @@ class TypedErrorsTest { arrow.website.examples.exampleTypedErrors21.example() } + @Test fun ExampleTypedErrors22() = runTest { + arrow.website.examples.exampleTypedErrors22.example() + } + } From f45caad4e4d95684c28850d7c01a8466d468f9f8 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 16 Oct 2024 20:56:30 +0200 Subject: [PATCH 17/22] Remove mentions of pattern matching --- content/blog/2024-10-04-arrow-2-0.md | 28 --- content/docs/learn/immutable-data/matching.md | 198 ------------------ content/docs/learn/overview.md | 3 +- .../kotlin/examples/example-matching-01.kt | 31 --- .../kotlin/examples/example-matching-02.kt | 25 --- 5 files changed, 1 insertion(+), 284 deletions(-) delete mode 100644 content/docs/learn/immutable-data/matching.md delete mode 100644 guide/src/test/kotlin/examples/example-matching-01.kt delete mode 100644 guide/src/test/kotlin/examples/example-matching-02.kt diff --git a/content/blog/2024-10-04-arrow-2-0.md b/content/blog/2024-10-04-arrow-2-0.md index cddf2264..75c9574f 100644 --- a/content/blog/2024-10-04-arrow-2-0.md +++ b/content/blog/2024-10-04-arrow-2-0.md @@ -23,7 +23,6 @@ empower developers to write more succinct and readable code. * [Additions to Fx](#additions-to-fx) * [Clearer retries for particular exceptions](#clearer-retries-for-particular-exceptions) * [Improved optics](#improved-optics) (breaking changes) -* [Pattern matching](#pattern-matching) * [Better support for kotlinx.serialization](#better-support-for-kotlinxserialization) ## Simple accumulation in Raise @@ -146,33 +145,6 @@ brings an improved variant that requires no arguments if the type of the `Iterable` is known. Similar improvements have been applied to `.at` and `.index`. -## Pattern matching - -One completely new feature in Arrow 2.0 is the [_pattern matching_ DSL](/learn/immutable-data/matching/). -By combining prisms and lenses one can specify a complex shape, and -then check whether a value fits into that shape, extracting some -pieces of information on the go. The DSL gets quite close to pattern -matching found in functional languages like Haskell or Scala. - -We do not intend this package to be a replacement for `when` expressions, -smart casts, and guards already provided by the language. On the other -hand, we acknowledge that pattern matching offers advantages when the -code needs to inspect very nested data. - -```kotlin -val User.name: String get() = this.matchOrThrow { - // Company(name = nm, director = Name(lastName = d)) - User.company(Company.name, Company.director(Name.lastName)) then { (nm, d) -> "$nm, att. $d" } - // Person(Name(firstName = fn), age if it < 18) - User.person(Person.name(Name.firstName), Person.age.suchThat { it < 18 }) then { (fn, _) -> fn } - // Person(Name(firstName = fn, lastName = ln)) -> "Sir/Madam $fn $ln" - User.person(Person.name(Name.firstName, Name.lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } -} -``` - -We also provide a new package, [`arrow-match`](/learn/immutable-data/matching/#matching-without-optics), which provides the same -pattern matching DSL, but using Kotlin's reflection instead of optics. - ## Better support for kotlinx.serialization Using Arrow Core data types as part of serialized data requires additional integration. diff --git a/content/docs/learn/immutable-data/matching.md b/content/docs/learn/immutable-data/matching.md deleted file mode 100644 index dde233d0..00000000 --- a/content/docs/learn/immutable-data/matching.md +++ /dev/null @@ -1,198 +0,0 @@ ---- -sidebar_position: 7 ---- - -# (Pattern) matching - -[Optionals](optional.md) and [prisms](prism-iso.md) provide ways to query -whether a certain piece of data is present, and [lenses](lens.md) allow -us to extract some information from a value. In many functional languages, -those tasks are accomplished using [pattern matching](https://en.wikipedia.org/wiki/Pattern_matching); -and with Arrow Optics those same ideas translate to Kotlin. - - - - - -In the rest of this document we use this set of types, which showcase a -sealed hierarchy (`User`/`Person`/`Company`) for which prisms are generated, -and data classes for which we get lenses. - -```kotlin -@optics data class Name( - val firstName: String, val lastName: String -) { companion object } - -@optics sealed interface User { companion object } -@optics data class Person( - val name: Name, val age: Int -): User { companion object } -@optics data class Company( - val name: String, val director: Name, val address: String -): User { companion object } -``` - -Here is the implementation of a small function that returns how to show -the name of a `User`. We use pattern matching to extract the information; -in particular, we pattern match on `this`, as hinted by the `this.matchOrThrow` -heading the code. - -```kotlin -val User.name: String get() = this.matchOrThrow { - // Company(name = nm, director = Name(lastName = d)) - User.company(Company.name, Company.director(Name.lastName)) then { (nm, d) -> "$nm, att. $d" } - // Person(Name(firstName = fn), age if it < 18) - User.person(Person.name(Name.firstName), Person.age.takeIf { it < 18 }) then { (fn, _) -> fn } - // Person(Name(firstName = fn, lastName = ln)) - User.person(Person.name(Name.firstName, Name.lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } -} -``` - -Let us unpack the block right after `this.matchOrThrow`, which defines three -potential _cases_. Each case is made up of two elements separated by `then`: - -1. The _pattern_ describes a shape of data that the value should be - matched against; -2. The _body_ describes the code to execute if the match is successful. - In that body, you have access to pieces of the data from the pattern. - -One way to describe a pattern is using a diagram, where each node represents a value of a type, -and each edge represents a lens or prism to be applied to a value. The diagram corresponding -to our first case looks as follows. - -```mermaid -flowchart TD - start[User] - company[Company] - name[String → nm] - director[Name] - lastName[String → d] - start -->|"User.company"| company - company -->|"Company.name"| name - company -->|"Company.director"| director - director --> |"Name.lastName"| lastName -``` - -As you can see, you can match more than one field inside a value by joining different optics -inside parentheses. This is what we do above with the name of the company, and the last name -of the director. After digging into the value, you always reach some _terminal_ values, -which are represented in the diagram as nodes with no children. Those terminal values are the -ones available as arguments to the body of the case; above we show the corresponding variable -names in the body. - -The pattern matching mechanism in `arrow-optics` also allow you to add checks about values -at any point in the pattern. In our example above, the `age` is checked to decide whether to -apply the first match, using `.takeIf { it < 18 }`. - -:::tip It - -If you do not need to perform an initial check with a prism, as we do above, you use -`it` at the start of the pattern. For example, `it(Person.name(Person.firstName))` would -match on a `Person` and obtain its first name. - -::: - -## Default cases - -The example at the beginning of this section is _exhaustive_, that is, matches every possible -value of type `User`. This may not always be the case; using `matchOrThrow` over a value which -is not handled by any pattern results in a `MatchNotFound` exception. - -If you want to define how to handle any remaining case, you use use the special `default` pattern. -For example, here we gather information about the director if available, and return `null` otherwise. - -```kotlin -fun User.directorOrNull(): Name? = this.matchOrThrow { - User.company(Company.director) then { it } - default { null } -} -``` - - -:::warning Exhaustiveness checking - -It is responsibility of the developer to ensure that the patterns cover every possible case, -adding `default` whenever required. - -The Kotlin compiler ensures that every `when` with a subject is ehxaustive using a specialized -analysis. Unfortunately, such an analysis is not performed over the patterns described in -this section. - -::: - -## Matching without optics - -You can also use pattern matching without bringing the full optics machinery in, by using -the reflection facilities provided by the Kotlin language alongisde the `arrow-match` package. - -:::tip Type-safe reflection - -Even though the term _reflection_ usually evokes unsafety, the API provided by `arrow-match` -guarantees that patterns that compile are well-behaved. - -::: - -Since we do not have optics, we need another way to represent types and property: - -- Instead of prisms, the type is chosen using `T::class`; -- Instead of lenses, properties are described using `T::property`. -- Further matching on a value is done using `.of`. - -You can see those changes in action in the "translation" of the first example in this section -to `arrow-match`. - - - -```kotlin -val User.name: String get() = this.matchOrThrow { - // Company(name = nm, director = Name(lastName = d)) - Company::class.of(Company::name, Company::director.of(Name::lastName)) then { (nm, d) -> "$nm, att. $d" } - // Person(Name(firstName = fn), age if it < 18) - Person::class.of(Person::name.of(Name::firstName), Person::age.takeIf { it < 18 }) then { (fn, _) -> fn } - // Person(Name(firstName = fn, lastName = ln)) - Person::class.of(Person::name.of(Name::firstName, Name::lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } -} -``` - - -## Matching with guards - -We strongly discourage using Arrow's pattern matching facilities for simple cases. -In most cases patterns can be recreated using `when` expressions with guards and -property access, leading to more idiomatic code. This approach has the additional -benefit of the exhaustiveness checking performed by the compiler. - -``` -val User.name: String get() = when (this) { - is Company -> "$name, att ${director.lastName}" - is Person if age < 18 -> name.firstName - is Person -> "Sir/Madam ${name.firstName} ${name.lastName}" -} -``` - -Here are some rules of thumb to decide when pattern matching is a good solution: - -- The decision over which branch to takes depends on nested information. - This is the case, for example, when working with abstract syntax trees (ASTs). -- One needs to obtain several sub-values. For example, `User.name` defined without - pattern matching forces us to write `name.firstName` and `name.lastName`, whereas - this duplication is gone when using a pattern. - -As you can see, the more nested your data it, the more pattern matching helps. diff --git a/content/docs/learn/overview.md b/content/docs/learn/overview.md index 35921754..06455d99 100644 --- a/content/docs/learn/overview.md +++ b/content/docs/learn/overview.md @@ -36,8 +36,7 @@ Each section in the documentation roughly corresponds to one of the libraries th | `arrow-fx-coroutines`
_Companion to [KotlinX Coroutines](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/)_ | [High-level concurrency](../coroutines/parallel), including `parMap` and `parZip`
[Resource management](../coroutines/resource-safety/) (with `suspend`) | | `arrow-autoclose` | [Resource management](../coroutines/resource-safety/) (no `suspend`) | | `arrow-resilience` | [Resilience patterns](../resilience/) | -| `arrow-optics` + `arrow-optics-ksp-plugin`
_Companion to [data](https://kotlinlang.org/docs/data-classes.html) and [sealed](https://kotlinlang.org/docs/sealed-classes.html) classes_ | Utilities for [immutable data](../immutable-data/intro/), including [pattern matching](../immutable-data/matching/) | -| `arrow-match` | [Pattern matching](../immutable-data/matching/) without optics | +| `arrow-optics` + `arrow-optics-ksp-plugin`
_Companion to [data](https://kotlinlang.org/docs/data-classes.html) and [sealed](https://kotlinlang.org/docs/sealed-classes.html) classes_ | Utilities for [immutable data](../immutable-data/intro/) | | `arrow-fx-stm` | [Software Transactional Memory](../coroutines/stm/) (STM) | | `arrow-atomic`
_Multiplatform-ready [references](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-atomic-reference/)_ | [Atomic references](../coroutines/concurrency-primitives/#atomic) | | `arrow-collectors`
_Companion to [`fold`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/fold.html) and [`reduce`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/reduce.html)_ | [Aggregation with single traversal](../collections-functions/collectors/) | diff --git a/guide/src/test/kotlin/examples/example-matching-01.kt b/guide/src/test/kotlin/examples/example-matching-01.kt deleted file mode 100644 index 340a3f4f..00000000 --- a/guide/src/test/kotlin/examples/example-matching-01.kt +++ /dev/null @@ -1,31 +0,0 @@ -// This file was automatically generated from matching.md by Knit tool. Do not edit. -package arrow.website.examples.exampleMatching01 - -import arrow.optics.* -import arrow.optics.match.* - -@optics data class Name( - val firstName: String, val lastName: String -) { companion object } - -@optics sealed interface User { companion object } -@optics data class Person( - val name: Name, val age: Int -): User { companion object } -@optics data class Company( - val name: String, val director: Name, val address: String -): User { companion object } - -val User.name: String get() = this.matchOrThrow { - // Company(name = nm, director = Name(lastName = d)) - User.company(Company.name, Company.director(Name.lastName)) then { (nm, d) -> "$nm, att. $d" } - // Person(Name(firstName = fn), age if it < 18) - User.person(Person.name(Name.firstName), Person.age.takeIf { it < 18 }) then { (fn, _) -> fn } - // Person(Name(firstName = fn, lastName = ln)) - User.person(Person.name(Name.firstName, Name.lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } -} - -fun User.directorOrNull(): Name? = this.matchOrThrow { - User.company(Company.director) then { it } - default { null } -} diff --git a/guide/src/test/kotlin/examples/example-matching-02.kt b/guide/src/test/kotlin/examples/example-matching-02.kt deleted file mode 100644 index 67b00134..00000000 --- a/guide/src/test/kotlin/examples/example-matching-02.kt +++ /dev/null @@ -1,25 +0,0 @@ -// This file was automatically generated from matching.md by Knit tool. Do not edit. -package arrow.website.examples.exampleMatching02 - -import arrow.match.* - -data class Name( - val firstName: String, val lastName: String -) - -sealed interface User -data class Person( - val name: Name, val age: Int -): User -data class Company( - val name: String, val director: Name, val address: String -): User - -val User.name: String get() = this.matchOrThrow { - // Company(name = nm, director = Name(lastName = d)) - Company::class.of(Company::name, Company::director.of(Name::lastName)) then { (nm, d) -> "$nm, att. $d" } - // Person(Name(firstName = fn), age if it < 18) - Person::class.of(Person::name.of(Name::firstName), Person::age.takeIf { it < 18 }) then { (fn, _) -> fn } - // Person(Name(firstName = fn, lastName = ln)) - Person::class.of(Person::name.of(Name::firstName, Name::lastName)) then { (fn, ln) -> "Sir/Madam $fn $ln" } -} From 38e9cf22b0d92cd38169d18395e24f4bab728f93 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 16 Oct 2024 20:59:46 +0200 Subject: [PATCH 18/22] Suggestions by @nomisRev --- content/blog/2024-10-04-arrow-2-0.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/content/blog/2024-10-04-arrow-2-0.md b/content/blog/2024-10-04-arrow-2-0.md index 75c9574f..88513ed2 100644 --- a/content/blog/2024-10-04-arrow-2-0.md +++ b/content/blog/2024-10-04-arrow-2-0.md @@ -29,7 +29,7 @@ empower developers to write more succinct and readable code. One of the core concepts when working with typed errors is the distinction between fail-first and [accumulation of errors](/learn/typed-errors/working-with-typed-errors/#accumulating-different-computations). Until now, the latter mode -required using `parZip` and `parMap`, which sometimes obscure the actual +required using `zipOrAccumulate` and `mapOrAccumulate`, which sometimes obscure the actual flow of the computation. In Arrow 2.0 we have sprinkled some DSL dust over `Raise`, and now you can @@ -38,13 +38,13 @@ general, any `RaiseAccumulate`) you use `by accumulating` to execute some computation keeping all the errors. ```kotlin -// version with parZip -parZip( +// version with `zipOrAccumulate` +zipOrAccumulate( { checkOneThing() }, { checkOtherThing() } ) { a, b -> doSomething(a, b) } -// version with accumulate +// version with `accumulate` accumulate { val a by accumulating { checkOneThing() } val b by accumulating { checkOtherThing() } From 6248e7e06a1dd0130943cb8abbd669ac921cf10d Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Tue, 12 Nov 2024 13:48:11 +0100 Subject: [PATCH 19/22] Update to 2.0.0-beta.1 --- content/blog/2024-10-04-arrow-2-0.md | 26 ++++++++++++++++++++++---- gradle/libs.versions.toml | 9 ++++----- guide/build.gradle.kts | 1 - 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/content/blog/2024-10-04-arrow-2-0.md b/content/blog/2024-10-04-arrow-2-0.md index 88513ed2..295194d3 100644 --- a/content/blog/2024-10-04-arrow-2-0.md +++ b/content/blog/2024-10-04-arrow-2-0.md @@ -7,9 +7,6 @@ tags: [core, articles] # Arrow 2.0 release We are happy to announce the next major release of Arrow, version 2.0! -As previously announced, migrating your projects to this release should be hassle-free -if your code compiled in 1.2.x without any deprecation warnings -(except for the breaking change in optics generation discussed below). This release is built with the new K2 compiler, and this gives us the ability to support a wider range of platforms, including WebAssembly. From now on, we shall @@ -25,6 +22,21 @@ empower developers to write more succinct and readable code. * [Improved optics](#improved-optics) (breaking changes) * [Better support for kotlinx.serialization](#better-support-for-kotlinxserialization) +## Upgrading to 2.0 + +As previously announced, migrating your projects to this release should be hassle-free +if your code compiled in 1.2.x without any deprecation warnings. Note that we talk about +**source** compatibility here, we had to break **binary** compatibility in several places +to implement improvements, such as in `NonEmptyList` and [`Schedule`](https://github.com/arrow-kt/arrow/pull/3504). + +There are two exceptions to this seamless transition. First, it was discovered that some +functions for `Map` in `Raise` collide with those of the standard library. Furthermore, +Arrow's variants return other `Map`, whereas the ones in the standard library return `List`. +The decision was to [rename them](https://github.com/arrow-kt/arrow/pull/3512/files#diff-b378045af72d02f1e5d4037d411102fcdb768239abeabedf69a4520b74ad0278). + +The second breaking change is related to [improved optics](#improved-optics), please +consult that section for further information. + ## Simple accumulation in Raise One of the core concepts when working with typed errors is the distinction @@ -124,7 +136,7 @@ second one using `_`. ## Improved optics -The two **breaking changes** in Arrow 2.0 relate to optics. +The largest **breaking changes** in Arrow 2.0 relate to optics. First of all, the [optics hierarchy](/learn/immutable-data/intro/#many-optics-to-rule-them-all) has been greatly simplified: now we have traversals, optionals, lenses, prisms, and isos, and no more intermediate types. This smaller amount of types means that the type of @@ -139,6 +151,12 @@ In the 1.x series, a value of type `String?` would be presented as behavior you should apply `.notNull` after the optic corresponding to the field. +A smaller breaking change is that generated optics are no longer +[inlined by default](https://github.com/arrow-kt/arrow/pull/3505). +This should prevent a large amount of warnings in which the compiler +complain that inlining is not significant. Note that the previous behavior +is still available under a flag. + One pain point when building [traversals](/learn/immutable-data/traversal/) was the need to provide an argument to `.every`, like `.every(Every.list())`. This new version brings an improved variant that requires no arguments if the type diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a61807fb..0ee4c5c5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] coroutines = "1.9.0" -kotest = "5.9.1" -kotlin = "2.0.21" +kotest = "6.0.0.M1" +kotlin = "2.1.0-RC" knit = "0.5.0" -arrow = "2.0.0-alpha.4" -ksp = "2.0.21-1.0.25" +arrow = "2.0.0-beta.1" +ksp = "2.1.0-RC-1.0.27" suspendapp = "0.4.0" kotlinKafka = "0.4.0" cache4k = "0.13.0" @@ -32,7 +32,6 @@ arrow-collectors = { module = "io.arrow-kt:arrow-collectors", version.ref = "arr arrow-eval = { module = "io.arrow-kt:arrow-eval", version.ref = "arrow" } arrow-functions = { module = "io.arrow-kt:arrow-functions", version.ref = "arrow" } arrow-cache4k = { module = "io.arrow-kt:arrow-cache4k", version.ref = "arrow" } -arrow-match = { module = "io.arrow-kt:arrow-match", version.ref = "arrow" } kotlinx-knit = { module = "org.jetbrains.kotlinx:kotlinx-knit", version.ref = "knit" } kotlinx-knit-test = { module = "org.jetbrains.kotlinx:kotlinx-knit-test", version.ref = "knit" } suspendapp = { module = "io.arrow-kt:suspendapp", version.ref = "suspendapp" } diff --git a/guide/build.gradle.kts b/guide/build.gradle.kts index 24efffea..3cddacb2 100644 --- a/guide/build.gradle.kts +++ b/guide/build.gradle.kts @@ -31,7 +31,6 @@ dependencies { testImplementation(libs.arrow.eval) testImplementation(libs.arrow.functions) testImplementation(libs.arrow.cache4k) - testImplementation(libs.arrow.match) testImplementation(libs.kotest.assertions.core) testImplementation(libs.kotest.property) testImplementation(libs.suspendapp) From 2b84c770028b3e6d67505156773fd57196f73dfb Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 4 Dec 2024 18:28:09 +0100 Subject: [PATCH 20/22] Add YouTube video --- ...2024-10-04-arrow-2-0.md => 2024-12-05-arrow-2-0.md} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename content/blog/{2024-10-04-arrow-2-0.md => 2024-12-05-arrow-2-0.md} (95%) diff --git a/content/blog/2024-10-04-arrow-2-0.md b/content/blog/2024-12-05-arrow-2-0.md similarity index 95% rename from content/blog/2024-10-04-arrow-2-0.md rename to content/blog/2024-12-05-arrow-2-0.md index 295194d3..f5e1a831 100644 --- a/content/blog/2024-10-04-arrow-2-0.md +++ b/content/blog/2024-12-05-arrow-2-0.md @@ -16,11 +16,11 @@ Apart from stabilization and general bug fixing, the theme of this release is improving the different DSLs provided by Arrow libraries. Our goal is to empower developers to write more succinct and readable code. -* [Simple accumulation in Raise](#simple-accumulation-in-raise) (experimental) -* [Additions to Fx](#additions-to-fx) -* [Clearer retries for particular exceptions](#clearer-retries-for-particular-exceptions) -* [Improved optics](#improved-optics) (breaking changes) -* [Better support for kotlinx.serialization](#better-support-for-kotlinxserialization) +
+

+ +

+
## Upgrading to 2.0 From 42036fd13e64fb29bd3034abbf72ee7b5afc66ce Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 5 Dec 2024 09:49:57 +0100 Subject: [PATCH 21/22] Remove image from blog post --- content/blog/2024-12-05-arrow-2-0.md | 2 ++ src/theme/BlogPostItem/Header/Image/index.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/content/blog/2024-12-05-arrow-2-0.md b/content/blog/2024-12-05-arrow-2-0.md index f5e1a831..1b914fae 100644 --- a/content/blog/2024-12-05-arrow-2-0.md +++ b/content/blog/2024-12-05-arrow-2-0.md @@ -1,6 +1,8 @@ --- title: Arrow 2.0 release +image: https://xebia.com/wp-content/uploads/2023/04/arrow-release-ftr.jpg category: articles +no_image_on_post: true tags: [core, articles] --- diff --git a/src/theme/BlogPostItem/Header/Image/index.tsx b/src/theme/BlogPostItem/Header/Image/index.tsx index 06c79034..11160288 100644 --- a/src/theme/BlogPostItem/Header/Image/index.tsx +++ b/src/theme/BlogPostItem/Header/Image/index.tsx @@ -49,7 +49,7 @@ export default function BlogPostItemHeaderImage(): JSX.Element | null { const image = assets.image ?? frontMatter.image; const { link } = frontMatter as BlogPostFrontMatterExpanded; - if (!image) { + if (!image || frontMatter.no_image_on_post) { return null; } From 525776c1760ebe4e6e0d937de4bcfe5f8b1b1cc8 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 5 Dec 2024 17:48:51 +0100 Subject: [PATCH 22/22] 2.0.0! --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b144ffce..158e7060 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ coroutines = "1.9.0" kotest = "6.0.0.M1" kotlin = "2.1.0" knit = "0.5.0" -arrow = "2.0.0-rc.1" +arrow = "2.0.0" ksp = "2.1.0-1.0.29" suspendapp = "0.4.0" kotlinKafka = "0.4.0"