beginner

Optics can compose to create more powerful optics and access deeply nested data structures in an seamless manner. The result of the composition of the 8 pairs of optics that are included in Bow is shown in the following table. Notice that not all combinations are possible for composition, and that some times the resulting optic is not the same as the optics that were composed.

Β | Iso |
Lens |
Prism |
AffineTraversal |
Getter |
Setter |
Fold |
Traversal |
---|---|---|---|---|---|---|---|---|

Iso |
Iso | Lens | Prism | AffineTraversal | Getter | Setter | Fold | Traversal |

Lens |
Lens | Lens | AffineTraversal | AffineTraversal | Getter | Setter | Fold | Traversal |

Prism |
Prism | AffineTraversal | Prism | AffineTraversal | π« | Setter | Fold | Traversal |

AffineTraversal |
AffineTraversal | AffineTraversal | AffineTraversal | AffineTraversal | Fold | Setter | Fold | Traversal |

Getter |
Getter | Getter | π« | π« | Getter | π« | Fold | π« |

Setter |
Setter | Setter | Setter | Setter | π« | Setter | π« | Setter |

Fold |
Fold | Fold | Fold | Fold | Fold | π« | Fold | Fold |

Traversal |
Traversal | Traversal | Traversal | Traversal | Fold | Setter | Fold | Traversal |

Each optic can be composed using its instance method `compose`

, which is overloaded to accept all possible combinations with other optics. Besides, for the sake of simplicity, operator `+`

can also be used to compose any two optics.

An N-ary tree is a tree where each node can have any number of branches. We can model it like:

```
enum NTree<A> {
case leaf(A)
indirect case node(A, branches: NEA<NTree<A>>)
}
```

That is, we can have a leaf with an associated value, or a node with an associated value and a `NonEmptyArray`

of branches (it must have at least one, otherwise it would be a leaf).

Letβs imagine that we would like to combine all values of the nodes at level `m`

. Stop for a moment and think how you would do it without optics. It does not have a trivial solution, right? Letβs see if we can leverage the power of optics to do this.

We can start by trying to access the branches of a node. Since `NTree`

is a sum type, the optic that we need to use is a Prism. We can use `AutoPrism`

to get the `Prism`

for the node side of the `NTree`

:

```
extension NTree: AutoPrism {}
func nodePrism<A>() -> Prism<NTree<A>, (A, NEA<NTree<A>>)> {
NTree.prism(for: NTree.node)
}
```

`nodePrism`

gives us a `Prism`

to look into a `NTree`

and get a pair of `(A, NEA<NTree<A>>)`

, but we are only interested in the branches part. Given we have a tuple, we would need an optic that lets us focus on one of the components of a tuple.

Fortunately, Bow already provides these utilities. In particular, given that tuples are product types, it seems we would need a `Lens`

to focus on the second component of the tuple. To do so, we can get the `Lens`

from `Tuple2._1`

. There are utilities like this from `Tuple2`

to `Tuple10`

, to focus on every component of the tuples.

Then, we can compose the previous `Prism`

with this `Lens`

to get an `AffineTraversal`

(see table above) that focuses only on the branches of a node:

```
func branchesAffineTraversal<A>() -> AffineTraversal<NTree<A>, NEA<NTree<A>>> {
nodePrism() + Tuple2._1
}
```

Now, we would like to be able to traverse each individual branch and modify them in isolation. This would give us a way of visiting the branches under the first level of the tree. If we look into the focus of the `branchesAffineTraversal`

we can see that it is a `NonEmptyArray`

, which already has a `Traversal`

to visit each element. Therefore, if we compose them, we can get a `Traversal`

with foci in each node of the first level under the provided node:

```
func levelTraversal<A>() -> Traversal<NTree<A>, NTree<A>> {
branchesAffineTraversal() + NEA.traversal
}
```

Looking at `levelTraversal`

we can see that its source and focus types match. That means we can compose with itself in order to go further down in the tree structure, level by level. We can write a function that gets us a `Traversal`

focused on the nodes of the `m`

level, just by composing the `levelTraversal`

with itself `m`

times:

```
func level<A>(_ m: UInt) -> Traversal<NTree<A>, NTree<A>> {
(0 ..< m)
.map { _ in levelTraversal() }
.reduce(Traversal.identity, +)
}
```

In the function above, if `m`

is 0, we return `Traversal.identity`

, which is a `Traversal`

that focuses on the source itself, corresponding to visiting level 0 of the tree. Otherwise, we create `m`

instances of the `levelTraversal`

and combine them all into a single one, to get a `Traversal`

that focuses on nodes at level `m`

.

We wanted to combine values of the nodes at level `m`

. First, we would need to extract the values out of the `NTrees`

. Since all cases in `NTree`

have a value, we can write a custom `Getter`

to do this:

```
func valueGetter<A>() -> Getter<NTree<A>, A> {
Getter(get: { state in
switch state {
case .leaf(let value), .node(let value, branches: _): return value
}
})
}
```

We can convert the `Traversal`

to a `Fold`

using the `asFold`

property. We can get a `Fold`

to combine values at level `m`

as:

```
func levelFold<A>(_ m: UInt) -> Fold<NTree<A>, A> {
level(m).asFold + valueGetter()
}
```

Finally, if we get a tree whose values have an instance of `Monoid`

, we can combine all its values at level `m`

by:

```
func combineValues<A: Monoid>(of tree: NTree<A>, at level: UInt) -> A {
levelFold(level).combineAll(tree)
}
```

With this example we have seen how we can use auto-generated, custom and library-provided optics, to build more complex ones that help us perfom a complicated task in an easy and seamless manner.