Skip to content

Commit c3fd2e1

Browse files
authored
Docs about arrow.optics.regex (#439)
1 parent 426d560 commit c3fd2e1

File tree

7 files changed

+512
-0
lines changed

7 files changed

+512
-0
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
sidebar_position: 7
3+
---
4+
5+
# Regular expressions
6+
7+
Lenses, prisms, and basic traversals have an important limitation: the only
8+
go _one level deep_ in the data structure. The functions in the
9+
`arrow.optics.regex` package remove that limitation, providing a great
10+
foundation for querying and modifying hierarchical data, such as trees
11+
or JSON documents.
12+
13+
<!--- TEST_NAME OpticsRegex -->
14+
15+
## Repetition
16+
17+
<!--- INCLUDE .*
18+
import arrow.optics.*
19+
import arrow.optics.dsl.*
20+
import arrow.optics.regex.*
21+
import arrow.optics.regex.dsl.*
22+
import io.kotest.matchers.shouldBe
23+
-->
24+
25+
We shall use different forms of trees in the examples below.
26+
The first variation only has data in the leaves.
27+
28+
```kotlin
29+
@optics sealed interface BinaryTree1<out A> {
30+
companion object
31+
}
32+
33+
@optics data class Node1<out A>(
34+
val children: List<BinaryTree1<A>>
35+
) : BinaryTree1<A> {
36+
constructor(vararg children: BinaryTree1<A>) : this(children.toList())
37+
38+
companion object
39+
}
40+
41+
@optics data class Leaf1<out A>(
42+
val value: A
43+
) : BinaryTree1<A> {
44+
companion object
45+
}
46+
```
47+
48+
Suppose we want to increment all the numbers in a binary tree of integers.
49+
The code below attempts to do that, but fails because it only traverses the
50+
children of nodes in one level -- this is why only the last `Leaf1` is
51+
modified after the call.
52+
53+
```kotlin
54+
val exampleTree1 = Node1(Node1(Leaf1(1), Leaf1(2)), Leaf1(3))
55+
56+
fun example() {
57+
val path = BinaryTree1.node1<Int>().children().every.leaf1().value()
58+
59+
val modifiedTree1 = path.modify(exampleTree1) { it + 1 }
60+
modifiedTree1 shouldBe Node1(Node1(Leaf1(1), Leaf1(2)), Leaf1(4))
61+
}
62+
```
63+
64+
<!--- KNIT example-optics-regex-01.kt -->
65+
<!--- TEST assert -->
66+
67+
<!--- INCLUDE .*
68+
@optics sealed interface BinaryTree1<out A> {
69+
companion object
70+
}
71+
72+
@optics data class Node1<out A>(
73+
val children: List<BinaryTree1<A>>
74+
) : BinaryTree1<A> {
75+
constructor(vararg children: BinaryTree1<A>) : this(children.toList())
76+
77+
companion object
78+
}
79+
80+
@optics data class Leaf1<out A>(
81+
val value: A
82+
) : BinaryTree1<A> {
83+
companion object
84+
}
85+
86+
val exampleTree1 = Node1(Node1(Leaf1(1), Leaf1(2)), Leaf1(3))
87+
-->
88+
89+
So how should be look at this problem? First, we know that we'll always
90+
end by traversing a final `Leaf1` and the `value` there. In the middle,
91+
we may need to go down the children once or more. In fact, _zero_
92+
times should also be considered, as the binary tree could be just a
93+
single leaf. We can express this idea by wrapping the first segment
94+
of the previous path with `zeroOrMore`.
95+
96+
```kotlin
97+
fun example() {
98+
val path = zeroOrMore(BinaryTree1.node1<Int>().children().every).leaf1().value()
99+
100+
val modifiedTree1 = path.modify(exampleTree1) { it + 1 }
101+
modifiedTree1 shouldBe Node1(Node1(Leaf1(2), Leaf1(3)), Leaf1(4))
102+
}
103+
```
104+
105+
<!--- KNIT example-optics-regex-02.kt -->
106+
<!--- TEST assert -->
107+
108+
The functions `zeroOrMore` and `onceOrMore` provide _repetition_ of
109+
a single lens, prims, or traversal, that is applied recursively.
110+
These functions are available on every scenario in which you can
111+
construct an optics going from a type to itself -- in our example,
112+
`node1<Int>().children().every` focus from `BinaryTree1` into
113+
`BinaryTree1`.
114+
115+
## Combination
116+
117+
Let's now consider another variation of binary trees, in which now
118+
at every step (leaf or node) we find a value.
119+
120+
```kotlin
121+
@optics sealed interface BinaryTree2<out A> {
122+
companion object
123+
}
124+
125+
@optics data class Node2<out A>(
126+
val innerValue: A,
127+
val children: List<BinaryTree2<A>>
128+
) : BinaryTree2<A> {
129+
constructor(value: A, vararg children: BinaryTree2<A>) : this(value, children.toList())
130+
131+
companion object
132+
}
133+
134+
@optics data class Leaf2<out A>(
135+
val value: A
136+
) : BinaryTree2<A> {
137+
companion object
138+
}
139+
```
140+
141+
If we construct a path similar to the previous one, we shall only
142+
focus on those values in leaves, as we can see in the example below.
143+
144+
```kotlin
145+
val exampleTree2 = Node2(1, Node2(2, Leaf2(3), Leaf2(4)), Leaf2(5))
146+
147+
fun example() {
148+
val path = zeroOrMore(BinaryTree2.node2<Int>().children().every).leaf2().value()
149+
150+
val modifiedTree2 = path.modify(exampleTree2) { it + 1 }
151+
modifiedTree2 shouldBe Node2(1, Node2(2, Leaf2(4), Leaf2(5)), Leaf2(6))
152+
}
153+
```
154+
155+
<!--- KNIT example-optics-regex-03.kt -->
156+
<!--- TEST assert -->
157+
158+
<!--- INCLUDE .*
159+
@optics sealed interface BinaryTree2<out A> {
160+
companion object
161+
}
162+
163+
@optics data class Node2<out A>(
164+
val innerValue: A,
165+
val children: List<BinaryTree2<A>>
166+
) : BinaryTree2<A> {
167+
constructor(value: A, vararg children: BinaryTree2<A>) : this(value, children.toList())
168+
169+
companion object
170+
}
171+
172+
@optics data class Leaf2<out A>(
173+
val value: A
174+
) : BinaryTree2<A> {
175+
companion object
176+
}
177+
178+
val exampleTree2 = Node2(1, Node2(2, Leaf2(3), Leaf2(4)), Leaf2(5))
179+
-->
180+
181+
The solution in this case is to _combine_ two different traversals into a single one.
182+
In the code below we build `nodeValues`, that focuses on values found in nodes,
183+
and `leafValues`, that focuses on those values in the leaves. Then we combine them
184+
using the `and` infix function from the library.
185+
186+
```kotlin
187+
fun example() {
188+
val nodeValues = zeroOrMore(BinaryTree2.node2<Int>().children().every).node2().innerValue()
189+
val leafValues = zeroOrMore(BinaryTree2.node2<Int>().children().every).leaf2().value()
190+
val path = nodeValues and leafValues
191+
192+
val modifiedTree2 = path.modify(exampleTree2) { it + 1 }
193+
modifiedTree2 shouldBe Node2(2, Node2(3, Leaf2(4), Leaf2(5)), Leaf2(6))
194+
}
195+
```
196+
197+
<!--- KNIT example-optics-regex-04.kt -->
198+
<!--- TEST assert -->
199+
200+
It is also possible to remove some of the duplication in the code above.
201+
In particular, we can separate the "digging" in the binary tree (the part where
202+
we use `zeroOrMore`) from obtaining the value by either looking at the one
203+
on a node or at the one on a leaf.
204+
205+
```kotlin
206+
fun example() {
207+
val pathToValue = BinaryTree2.node2<Int>().innerValue() and BinaryTree2.leaf2<Int>().value()
208+
val path = zeroOrMore(BinaryTree2.node2<Int>().children().every) compose pathToValue
209+
210+
val modifiedTree2 = path.modify(exampleTree2) { it + 1 }
211+
modifiedTree2 shouldBe Node2(2, Node2(3, Leaf2(4), Leaf2(5)), Leaf2(6))
212+
}
213+
```
214+
215+
<!--- KNIT example-optics-regex-05.kt -->
216+
<!--- TEST assert -->
217+
218+
Unfortunately, with this refactor we no longer can use the chained syntax
219+
using `.` all the time. We need to resort to `compose` instead.
220+
221+
:::info Regular expressions?
222+
223+
You might be wondering why we call `zeroOrMore`, `onceOrMore`, and `and` the
224+
_regular expression_ functions. To understand this, we need to view _paths_
225+
to data as "strings" in which each "letter" represent one single optic.
226+
227+
For example, if we use `n` for `node1`, `c` for `children`, `e` for `every`,
228+
`l` for `leaf1`, and `v` for `value`, the "strings" we want to use to access
229+
all the values are those of the form:
230+
231+
```
232+
lv
233+
ncelv
234+
ncencelv
235+
ncencencelv
236+
...
237+
```
238+
239+
From that point of view, the regular expression that matches all the possible
240+
paths we want to take is `(nce)*lv`, where `*` is the regular expression
241+
operator that matches that string zero or more times.
242+
243+
Similarly, `onceOrMore` corresponds to `+`. The `and` functions corresponds
244+
to `+`, since it allows choosing between several matches, that in turn
245+
correspond to different possible paths to focus on the data.
246+
247+
:::
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// This file was automatically generated from regex.md by Knit tool. Do not edit.
2+
package arrow.website.examples.exampleOpticsRegex01
3+
4+
import arrow.optics.*
5+
import arrow.optics.dsl.*
6+
import arrow.optics.regex.*
7+
import arrow.optics.regex.dsl.*
8+
import io.kotest.matchers.shouldBe
9+
10+
@optics sealed interface BinaryTree1<out A> {
11+
companion object
12+
}
13+
14+
@optics data class Node1<out A>(
15+
val children: List<BinaryTree1<A>>
16+
) : BinaryTree1<A> {
17+
constructor(vararg children: BinaryTree1<A>) : this(children.toList())
18+
19+
companion object
20+
}
21+
22+
@optics data class Leaf1<out A>(
23+
val value: A
24+
) : BinaryTree1<A> {
25+
companion object
26+
}
27+
28+
val exampleTree1 = Node1(Node1(Leaf1(1), Leaf1(2)), Leaf1(3))
29+
30+
fun example() {
31+
val path = BinaryTree1.node1<Int>().children().every.leaf1().value()
32+
33+
val modifiedTree1 = path.modify(exampleTree1) { it + 1 }
34+
modifiedTree1 shouldBe Node1(Node1(Leaf1(1), Leaf1(2)), Leaf1(4))
35+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// This file was automatically generated from regex.md by Knit tool. Do not edit.
2+
package arrow.website.examples.exampleOpticsRegex02
3+
4+
import arrow.optics.*
5+
import arrow.optics.dsl.*
6+
import arrow.optics.regex.*
7+
import arrow.optics.regex.dsl.*
8+
import io.kotest.matchers.shouldBe
9+
@optics sealed interface BinaryTree1<out A> {
10+
companion object
11+
}
12+
13+
@optics data class Node1<out A>(
14+
val children: List<BinaryTree1<A>>
15+
) : BinaryTree1<A> {
16+
constructor(vararg children: BinaryTree1<A>) : this(children.toList())
17+
18+
companion object
19+
}
20+
21+
@optics data class Leaf1<out A>(
22+
val value: A
23+
) : BinaryTree1<A> {
24+
companion object
25+
}
26+
27+
val exampleTree1 = Node1(Node1(Leaf1(1), Leaf1(2)), Leaf1(3))
28+
29+
fun example() {
30+
val path = zeroOrMore(BinaryTree1.node1<Int>().children().every).leaf1().value()
31+
32+
val modifiedTree1 = path.modify(exampleTree1) { it + 1 }
33+
modifiedTree1 shouldBe Node1(Node1(Leaf1(2), Leaf1(3)), Leaf1(4))
34+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// This file was automatically generated from regex.md by Knit tool. Do not edit.
2+
package arrow.website.examples.exampleOpticsRegex03
3+
4+
import arrow.optics.*
5+
import arrow.optics.dsl.*
6+
import arrow.optics.regex.*
7+
import arrow.optics.regex.dsl.*
8+
import io.kotest.matchers.shouldBe
9+
@optics sealed interface BinaryTree1<out A> {
10+
companion object
11+
}
12+
13+
@optics data class Node1<out A>(
14+
val children: List<BinaryTree1<A>>
15+
) : BinaryTree1<A> {
16+
constructor(vararg children: BinaryTree1<A>) : this(children.toList())
17+
18+
companion object
19+
}
20+
21+
@optics data class Leaf1<out A>(
22+
val value: A
23+
) : BinaryTree1<A> {
24+
companion object
25+
}
26+
27+
val exampleTree1 = Node1(Node1(Leaf1(1), Leaf1(2)), Leaf1(3))
28+
29+
@optics sealed interface BinaryTree2<out A> {
30+
companion object
31+
}
32+
33+
@optics data class Node2<out A>(
34+
val innerValue: A,
35+
val children: List<BinaryTree2<A>>
36+
) : BinaryTree2<A> {
37+
constructor(value: A, vararg children: BinaryTree2<A>) : this(value, children.toList())
38+
39+
companion object
40+
}
41+
42+
@optics data class Leaf2<out A>(
43+
val value: A
44+
) : BinaryTree2<A> {
45+
companion object
46+
}
47+
48+
val exampleTree2 = Node2(1, Node2(2, Leaf2(3), Leaf2(4)), Leaf2(5))
49+
50+
fun example() {
51+
val path = zeroOrMore(BinaryTree2.node2<Int>().children().every).leaf2().value()
52+
53+
val modifiedTree2 = path.modify(exampleTree2) { it + 1 }
54+
modifiedTree2 shouldBe Node2(1, Node2(2, Leaf2(4), Leaf2(5)), Leaf2(6))
55+
}

0 commit comments

Comments
 (0)