Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Immutable data #49

Merged
merged 20 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion content/docs/learn/design/_category_.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"label": "Design",
"position": 6,
"position": 7,
"link": {
"type": "generated-index",
"description": "Recipes for designing nice Kotlin code"
Expand Down
8 changes: 8 additions & 0 deletions content/docs/learn/immutable-data/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"label": "Immutable data ⇄ Optics",
"position": 6,
"link": {
"type": "generated-index",
"description": "Optics and other ways to handle immutable data"
}
}
151 changes: 151 additions & 0 deletions content/docs/learn/immutable-data/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
sidebar_position: 1
---

# Introduction

<!--- TEST_NAME ImmutableIntro -->

Data classes, sealed hierarchies, and above all of that **immutable data** is
a great recipe for [domain modeling](../../design/domain-modeling/). If we want
to model a domain sharply, we often end up with a large amount of (nested)
classes, each of them representing a particular kind of object.

```kotlin
data class Person(val name: String, val age: Int, val address: Address)
data class Address(val street: Street, val city: City)
data class Street(val name: String, val number: Int?)
data class City(val name: String, val country: String)
```

Alas, Kotlin doesn't provide great tooling out of the box to transform those
values. Data classes have a built-in `copy` method, but we need to repeat the
name of the fields, and perform iterated copies even if we only want to touch
one single field.

```kotlin
fun Person.capitalizeCountry() =
serras marked this conversation as resolved.
Show resolved Hide resolved
this.copy(
address = address.copy(
city = address.city.copy(
country = address.city.country.capitalize()
)
)
)
```
<!--- KNIT example-immutable-intro-01.kt -->

:::note

We often use the word _transform_ even though we are talking about immutable
data. In most cases we refer to creating a _copy_ of the value where some of
the fields are _modified_.

:::

## Meet optics

Arrow provides a solution to this problem in the form of **optics**. Optics
are values which represent access to a value (or values) inside a larger
value. For example, we may have an optic focusing (that's the term we use)
on the `address` field of a `Person`. By combining different optics we can
focus on nested elements, like the `city` field within the `address` field
within a `Person`. But code speaks louder than words, so let's see how the
example above improves using optics.

The easiest way to get started with Arrow Optics is through its compiler
plug-in. After [adding it to your build](../../quickstart/#additional-setup-for-plug-ins)
you just need to mark each class for which you want optics to be generated
with the `@optics` annotation.

```kotlin
import arrow.optics.*

@optics data class Person(val name: String, val age: Int, val address: Address) {
companion object
}
@optics data class Address(val street: Street, val city: City) {
companion object
}
@optics data class Street(val name: String, val number: Int?) {
companion object
}
@optics data class City(val name: String, val country: String) {
companion object
}
```

:::caution Annoying companion object

You need to have a `companion object` declaration in each class, even if it's empty.
This is due to limitations in [KSP](https://kotlinlang.org/docs/ksp-quickstart.html),
the compiler plug-in framework used to implement the Arrow Optics plug-in.

:::
nomisRev marked this conversation as resolved.
Show resolved Hide resolved

The plug-in generates optics for each field, available under the name of the class.
For example, `Person.address` is the optic focusing on the `address` field.
Furthermore, you can create optics which focus on nested fields by using the
same dot notation you're used to. In this case,
`Person.address.city.country` represents the optic focusing precisely on
the field we want to transform. Using it we can reimplement `capitalizeCountry`
in two ways:

1. **Optic-first**: the `modify` operation of an optic takes an entire value
(`this` in the example) and the transformation to apply to the focused element.

```kotlin
fun Person.capitalizeCountryModify() =
serras marked this conversation as resolved.
Show resolved Hide resolved
Person.address.city.country.modify(this) { it.capitalize() }
```

2. **Copy builder**: Arrow Optics provides an overload of `copy` which instead
of named arguments takes a block. Inside that block you can use the syntax
`optic transform operation` to modify a focused element.

```kotlin
fun Person.capitalizeCountryCopy() =
serras marked this conversation as resolved.
Show resolved Hide resolved
this.copy {
Person.address.city.country transform { it.capitalize() }
}
```

<!--- KNIT example-immutable-intro-02.kt -->

## Many optics to rule them all

You may have noticed that we speak about optic*s*. In fact, there are a few
different important kinds, which differ in the *amount* of elements they
can potentially focus on. All the optics in the example above are **lenses**,
which have exactly one focus. At the other end of the spectrum we have
[**traversals**](../traversal), which focus on any amount of elements; they can be used to
uniformly modify all the elements in a list, among other operations.
Optics form a hierarchy, which we can summarize in the diagram below.

<center>

```mermaid
graph TD;
traversal{{"<a href='../traversal/'>Traversal</a> (0 .. ∞)<br /><code>getAll</code> (return a list)<br /><code>modify</code> and <code>set</code>"}};
optional{{"<a href='../optional-prism/'>Optional</a> (0 .. 1)<br /><code>getOrNull</code> (return a nullable)"}};
lens{{"<a href='../lens/'>Lens</a> (exactly 1)<br /><code>get</code>"}};
prism{{"<a href='../optional-prism/#constructing-values'>Prism</a><br /><code>reverseGet</code>"}};
traversal-->optional;
optional-->lens;
optional-->prism;
```

</center>

The "main line" of optics is `Traversal` → `Optional` → `Lens`, which differ
only in the amount of elements they focus on. [`Prism`](../optional-prism) adds a small
twist: it allows not only modifying, but also _creating_ new values and
matching over them.
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if matching is a term that is as well known in Kotlin as Haskell or Scala. Can we somehow specify it's related to when?


:::info Even more optics

Arrow 1.x features a larger hierarchy of optics, because the operations of
"getting" values, and "modifying" them live in different interfaces.
Arrow 2.x simplifies the hierarchy to the four optics described in this section.

:::
Loading