Skip to content

Commit

Permalink
Corrects minor mistakes
Browse files Browse the repository at this point in the history
  • Loading branch information
Hauke Sommerfeld committed Sep 17, 2024
1 parent d14506a commit 02844d8
Showing 1 changed file with 51 additions and 50 deletions.
101 changes: 51 additions & 50 deletions www/src/pages/docs/50_StoreMapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ This can be used to identify your elements semantically (for validation or autom

If you have deep nested structures or a lot of them, you may want to automate this behavior.

fritz2 offers an annotation `@Lenses` you can add to your data-classes, sealed-classes and sealed-interfaces in the
fritz2 offers an `@Lenses` annotation you can add to your data-classes, sealed-classes and sealed-interfaces in the
`commonMain` source-set of your multiplatform project:
```kotlin
@Lenses
Expand Down Expand Up @@ -178,10 +178,11 @@ the browser and backend.

::: info
There are other lenses generated as well. Especially when dealing with `sealed`-hierarchies, fritz2 offers more
helpful variants that commonly needed to apply different idioms and patterns like *validation* or holding the base
helpful variants that are commonly needed to apply different idioms and patterns like *validation* or holding the base
type in some `Store` for type based UI design.

You can find out in the two applications in the sections [Dealing with Sealed Type Hierarchies](#dealing-with-sealed-type-hierarchies)
You can find out more in the two applications described in the sections
[Dealing with Sealed Type Hierarchies](#dealing-with-sealed-type-hierarchies)
and [Delegating Validation in Sealed Hierarchies](/docs/validation/#delegating-validation-in-sealed-hierarchies).
:::

Expand Down Expand Up @@ -490,7 +491,7 @@ to encapsulate the store mapping directly into the former presented convenience
### Dealing with sealed Type Hierarchies

There are special needs when you integrate `sealed` types into your model. It does not matter whether this is at the
root or nested inside your model-tree. There are always similar idioms that needs to be applied. Those idioms need
root or nested inside your model-tree. There are always similar idioms that need to be applied. Those idioms need
special lenses, that will be generated by our `lenses-annotation-processor` for you.

Imagine the following example model:
Expand All @@ -513,19 +514,19 @@ data class Wishlist(
}
```

As you can see, we want to model some wishlist, which holds simply a list of wishes.
As you can see, we want to model a wishlist, which simply holds a list of wishes.
A `Wish` is a `sealed interface` that defines a `label` for all common children. It is also annotated by the known
`@Lenses`-annotation in order to tell the annotation processor to create lenses for this base type too.

The `WhishList` simply serves as some root node of the model to integrate the sealed hierarchy into another type.
The `WhishList` simply serves as a root node of the model to integrate the sealed hierarchy into another type.

:::info
There is one restriction to our support:
All children of an annotated sealed base type need to be `data class`es that are themselves annotated by the `@Lenses`
annotation. The annotation processor will fail with an error, if this constraint is violated.
Our support for sealed type hierarchies comes with an exception:
All children of an annotated sealed base type need to be `data class`es that are themselves annotated with the `@Lenses`
annotation. The annotation processor will fail with an error if this constraint is violated.
:::

There might be different kind of wishes though, that all differ in their special properties.
There might be different kinds of wishes, though, which all differ in their special properties.
Take those for example:
```kotlin
@Lenses
Expand All @@ -549,10 +550,10 @@ data class LightSaber(
}
```

Our `WhishList.wishes` list can take arbitrary objects of `Computer` and `LightSaber`, while both types needs different
aspects to be well defined:
- a computer needs RAM - as we refer to good old homecomputer's era, we store those values in kilobytes.
- a light-saber needs a color; as fritz2's logo is rather *petrol*, we make this possible for our merch.
Our `WhishList.wishes` list can hold arbitrary objects of `Computer` and `LightSaber`, while both types need different
aspects to be well-defined:
- a computer needs RAM - as we refer to the good old homecomputer's era, we store those values in kilobytes.
- a lightsaber needs a color - as fritz2's logo is somewhat *petrol*, we make this possible for our merch.

Look at an example with a `Store` of type `WishList`:
```kotlin
Expand All @@ -571,22 +572,23 @@ We would like to implement a small UI to manage those wishes. It might look simi
![Wishlist App](/img/sealed-hierarchy-example-app.png)

The overall example is rather good [domain modelling](https://arrow-kt.io/learn/design/domain-modeling/),
but it imposes some problems, when it comes to store-mapping:
1. to map the main model down to the nodes of the tree (`Whislist -> wishes -> some single wish -> Computer -> ramInKb`),
there is the need to map a store of the base type to some specific type. As mapping involves `Lens`es, we need for
example some `Lens<Wish, Computer>`. How could this be done?
2. another difficulty arises by the fact, that we also want to modify the *common* properties of the base type. But
there is no implementation though, so how should we implement the `setter` of a `Lens<Wish, String>` for the
but it imposes some problems when it comes to store-mapping:

1. To map the main model down to the nodes of the tree (`Whislist -> wishes -> some single wish -> Computer -> ramInKb`),
there is the need to map a store of the base type to a specific type. As mapping involves `Lens`es, we need
a `Lens<Wish, Computer>`, for example. How could this be done?
2. Another difficulty arises by the fact that we also want to modify the *common* properties of the base type.
There is no implementation, though, so how should we implement the `setter` of a `Lens<Wish, String>` for the
`Whish.label`-property?

There are solutions with simple idioms for those two problems, that you can craft by hand. But fritz2's lenses
processor create the needed `Lens`es automatically for you.
There are solutions with simple idioms for those two problems that you can craft by hand. fritz2's lenses
processor creates the needed `Lens`es automatically for you, however.

The following two sections show and explain those solutions.
The following two sections demonstrate the aforementioned solutions.

#### Up-Casting and Down-Casting Lenses

Let us recap tht pathes we need to take for a store-mapping from top to bottom elements:
Let us recap the paths we need to take for a store-mapping from top to bottom elements:
- `Whislist -> wishes -> some single wish -> Computer -> ramInKb`
- `Whislist -> wishes -> some single wish -> LightSaber -> color`

Expand All @@ -611,13 +613,13 @@ val ramInKb: Lens<Computer, Int> = TODO()
val color: Lens<LightSaber, Color> = TODO()
```

The two problematic lenses are the ones, that need to implement the up-casting from the base type to the specific type:
The two problematic lenses are the ones that need to implement the up-casting from the base type to the specific type:
- `Lens<Wish, Computer>`
- `Lens<Wish, LightSaber>`

We definitely need to know the specific type, which we can typically solve by an appropriate `when`-expression.
We need to know the specific type which we can typically solve with an appropriate `when`-expression.
Remember the type notation of the `getter`-property of a `Lens<P, T>` is `(P) -> T` which would translate to
`(Wish) -> Computer` in our example case. To gain the correct return type, we can simply cast inside the
`(Wish) -> Computer` in our example case. To obtain the correct return type, we can simply cast inside the
`getter`-expression of our `Lens` to the specific type:
```kotlin
val computerLens: Lens<Wish, Computer> = lensOf(
Expand All @@ -633,11 +635,11 @@ As the `setter` has the type notion `(Wish, Computer) -> Wish` we can simply ret
the correct specific type.

:::info
As we cast from the base type to a more specific type, this is called up-casting.
And since we apply this to a lens, we call this kind of lens *up-casting* lens.
Casting from the base type to a more specific type is called up-casting.
Since we apply this to a lens, we call this kind of lens *up-casting* lens.
:::

Armed with such up-casting lenses, we can easily access or change values of our example `WishList`-object:
Armed with such an up-casting lenses, we can easily access or change values of our example `WishList`-object:
```kotlin
val wishlist = Wishlist(
"Christmas wishes",
Expand Down Expand Up @@ -674,11 +676,11 @@ println(ramInKbLens.get(wishlist)) // prints: 64
println(ramInKbLens.get(upgradedList)) // prints: 512
```

As we already have mentioned, fritz2's annotation processor will generate those up-casting lenses for you.
They are named like the classname with starting lowercase letter, thus `Computer` will become `computer()` as factory
name.
As mentioned before, fritz2's annotation processor will generate those up-casting lenses for you.
They are named after the classname starting with a lowercase letter. Thus, `Computer` will have `computer()` as it's
factory name.

We could now apply this to map the `Store` and provide some form elements to change the state:
We can now use this to map the `Store` and provide some form elements to change the state:
```kotlin
val storedWishList: Store<WishList> = storeOf(wishlist)
val storedWishes = storedWishList.map(Wishlist.wishes())
Expand Down Expand Up @@ -728,8 +730,8 @@ storedWishList.data.map { it.wishes.withIndex().toList() }.renderEach { (index,
As there may be use-cases where we need to access the other way round, fritz2 also creates so called *down-casting*
lenses for each child of a sealed base type.

Those are lenses, that have a specific type as parent and the base type as lens outcome.
Translated to the example there is a `Lens`-factory created on the type `LightSaber` called `wish()`, that looks like
Those are lenses, that have a specific type as their parent and the base type as the lens's outcome.
Translated to the example there is a `Lens`-factory created on the type `LightSaber` called `wish()` that looks like
this:

```kotlin
Expand All @@ -746,8 +748,8 @@ public fun LightSaber.Companion.wish(): Lens<LightSaber, Wish> = lensOf(

The second problem of sealed type hierarchies is the access of the *common* properties.

As a sealed base-type is an `interface` or an `abstract class` and *no* `data class`, there is no `copy()`-function
we could apply in order to provide a useful `setter` implementation!
As a sealed base-type is an `interface` or an `abstract class` and *not a* `data class`, there is no `copy()`-function
we could use in order to provide a useful `setter` implementation!

Let us illustrate the problem for the `Wish.label`-property:
```kotlin
Expand Down Expand Up @@ -789,15 +791,15 @@ val labelLens: Lens<Wish, String> = lensOf(
)
```

Because the work is *delegated* to the different children types, we call this kind of lens *delegating lens*.
As the work is *delegated* to the different child types, we call this kind of lens *delegating lens*.

As this is tedious to write, the fritz2's annotation processor will create those lenses for you. Because it needs to
be sure, it can use the `copy()`-functions, there is the restriction, that every child of a sealed base type needs
be sure it can use the `copy()`-functions, there is the restriction that every child of a sealed base type needs
to be a `data class`!

You have already heard about this at the start of top level section, but now you understand the reason for that.

Now we can provide some input-field for the common `label`-property, that gets mapped to the appropriate
Now we can provide an input-field for the common `label`-property that gets mapped to the appropriate
child object's field:

```kotlin
Expand All @@ -823,9 +825,9 @@ storedWishList.data.map { it.wishes.withIndex().toList() }.renderEach { (index,

#### Exclude Fields from Lenses Generation

Sometimes you may not want to create a `Lens` for all public fields of a sealed base type.
Typical use cases may be properties, that should be static for all instances, like some kind of screen
name of the type or alike.
Sometimes you may not want to create a `Lens` for each public field of a sealed base type.
Typical use cases may be properties that should be static for all instances, like some kind of display
name of the type.

For those cases fritz2 offers the `@NoLens`-annotation.

Expand Down Expand Up @@ -859,9 +861,9 @@ data class Computer(

#### Complete Wishlist Example

As this example is rather complex, we want you to show the full working implementation of the preceding wishlist
code snippets. The styling is done with [tailwindcss](https://tailwindcss.com/), so you can copy and paste it in
our well known [fritz2-tailwind-template](https://github.com/jwstegemann/fritz2-tailwind-template) project:
As this example is rather complex, the full working implementation of the preceding wishlist
code snippets can be found below. The styling is done with [TailwindCSS](https://tailwindcss.com/), so you can copy and
paste it in our well known [fritz2-tailwind-template](https://github.com/jwstegemann/fritz2-tailwind-template) project:

```kotlin
// somewhere in your `commonMain`-source-set:
Expand Down Expand Up @@ -1004,9 +1006,8 @@ render {
```

:::warning
The above example is intentionally kept very simply structured to reduce complexity and show the code mostly
straight forwarded.
The above example is intentionally kept very simple in to reduce complexity and show the code in a straight-forward way.

For real world application we encourage you to [structure](/docs/render/#structure-ui) it better and divide the UI
into smaller and reusable portions of course.
into smaller and reusable parts, of course.
:::

0 comments on commit 02844d8

Please sign in to comment.