The 90% you need to know to use optics
Tags: haskell, purescript, functional, scala
Published: 2023-03-30 14:31:42

📹 Hate reading articles? Check out [the complementary video](https://youtu.be/tIUXB0TrlpU ), which covers the same content.

---

The premise is simple. I think people go too crazy with optics. If you want to use optics, here is 90% of what you need:

- Lenses and traversals;
- How to compose them;
- How to create them;
- Use these four operators: `view`, `set`, `over`, and `toListOf`.

*(Or their alternatives in other language, more on that later).*

And that’s it! You get a colossal toolbox and a productivity boost in less than ten functions. The core knowledge is fundamental – it goes beyond libraries and languages.

---

💡 If you’ve read any optics tutorials before, you might wonder: What about prisms and other optics? Or what about different encodings? They seem to be so important.

No, they are not. Don’t waste your time when getting started. You can pick these up later (if you want to).

---

We have to start with boring parts. The first step is to pick and install the library. I’ll go with PureScript and [`profunctor-lenses`](https://pursuit.purescript.org/packages/purescript-profunctor-lenses/8.0.0) because it’s less noisy and nicer for demos. But the basic ideas apply to other languages, like Haskell and Scala.

```haskell
spago install profunctor-lenses
```

Then we have to prepare the data. Imagine that we’re working on some bar order service – we can start with a simple highball type.

---

🍸 Highball is a simple long drink with one liquor and one mixer.

---

```haskell
type Order =
  { drink :: Highball
  }

type Highball =
  { liquor :: Liquor
  , mixer :: Mixer
  , ounces :: Int
  }

data Liquor = Scotch | Gin

data Mixer = Soda | Tonic
```

And a couple of instances to show/see what we’re doing:

```haskell
derive instance Generic Liquor _
instance Show Liquor where
  show = genericShow

derive instance Generic Mixer _
instance Show Mixer where
  show = genericShow
```

## Lens

- A lens deconstructs product types, such as records and tuples.
- A lens must always focus on the value.

In our case, we can create a lens that focuses on the `drink` field (or part) of the `Order` record and lenses that focus on `liquor`, `mixer`, and `ounces` of `Highball`. Let’s write a couple.

*Disclaimer for Java boilerplate flashbacks. This is the only boilerplate we have to write.*

```haskell
drinkLens :: Lens' Order Highball
drinkLens = prop (Proxy :: _ "drink")

liquorLens :: Lens' Highball Liquor
liquorLens = prop (Proxy :: _ "liquor")
```

`drinkLens` is a lens from `Order`(whole) to `Highball`(part); we create it by using a function `prop` and passing it the name of the field `drink`. We use `Proxy` because it’s type-level information – if we pass a wrong non-existing field (for example, `drank`), it won’t compile.

### Lens composition

We can already use these, but they're boring and not exciting.

Optics are most useful for nested data structures. One of the most remarkable things about optics is composition. We can compose two lenses to get a “larger” lens.

Let’s compose `drinkLens` and `liquorLens` to get a lens from `Order` to `Liquor`.

```haskell
orderedLiquor :: Lens' Order Liquor
orderedLiquor = drinkLens <<< liquorLens
```

---

💡 `(<<<)` is the composition operator in PureScript.

---

Note that we don’t have to declare intermediate optics (when needed):

```haskell
orderedOunces :: Lens' Order Int
orderedOunces = drinkLens <<< prop (Proxy :: _ "ounces")
```

*And finally, we can see lenses in use.*

### Using lenses

We can use lenses to **get**, **set**, or **modify** a value within a structure when we know it exists.

Imagine we have an order:

```haskell
let myOrder = { drink: { liquor: Scotch, mixer: Soda, ounces: 10 } }
```

We can use the lens (from `Order` to `Liquor`) to **get** the value using the `view` function:

```haskell
view orderedLiquor myOrder 
-- Scotch
```

We can use the lens to **set** the value using the `set` function; for example, to switch to `Gin`:

```haskell
set orderedLiquor Gin myOrder
-- { drink: { liquor: Gin, mixer: Soda, ounces: 10 } }
```

We can use the lens to **modify** the value using the `over` function; for example, to bump the drink size:

```haskell
over orderedOunces (_ + 1) myOrder
-- { drink: { liquor: Scotch, mixer: Soda, ounces: 11 } }
```

*Which might remind you of using a `map` function.*

---

💡 Note that lenses aren’t limited to the same type, we can use type-changing lenses and operations, but we keep it simple for now.

---

### Quick recap

- A lens focuses on one part of the structure, such as a field of a record.
- We can compose two lenses to get another lens (using `(<<<)`).
- We can use the `props` function to create a lens in `purescript-profunctor-lenses`.
- We can use `view` to **get**, `set` to **set**, and `over` to **modify** values with a lens.

## Traversal

Let’s modify the `Order` type to allow multiple drinks instead of just one.

```haskell
type Order =
  { drinks :: Array Highball
  }
```

The code doesn't compile anymore – we have to fix our lenses.

The `drinkLens:: Lens' Order Highball` is invalid because the field's name and type have changed. We have to patch it:

```haskell
-- drinkLens :: Lens' Order Highball
-- drinkLens = prop (Proxy :: _ "drink")

drinksLens :: Lens' Order (Array Highball)
drinksLens = prop (Proxy :: _ "drinks")
```

The compiler is still not happy, pointing fingers at `orderedLiquorLens`:

```haskell
orderedLiquors :: Lens' Order Liquor
orderedLiquors = drinkLens <<< liquorLens
--                             ^^^^^^^^^^
-- Compilation error:
-- Could not match type Record with type Array
```

It says it could not match the type `Record` with the type `Array`: 
- `drinksLens` goes from `Order` to `Array Highball`;
- `liquorLens` goes from `Highball` to `Liquor`.

So, now we have a “gap” or a mismatch between `Array Highball` and `Highball`. We can’t use a lens here because a lens must always focus on the value. But arrays can have one, multiple, or even zero elements (values).

Here is where the traversals come in. **Traversal** focuses on multiple values in a structure (or collection).

### Making and composing traversals

To fix the code, we must add a traversal to the composition and change the type from `Lens'` to `Traversal'`. Composing a lens with a traversal or two traversals makes a traversal. 

```haskell
orderedLiquor :: Traversal' Order Liquor
orderedLiquor = drinksLens <<< traversed <<< liquorLens
```

The `traversed` function creates a traversal; we can extract it to make this explicit and clear:

```haskell
orderedLiquor :: Traversal' Order Liquor
orderedLiquor = drinksLens <<< allDrinksTraversal <<< liquorLens

allDrinksTraversal :: Traversal' (Array Highball) Highball
allDrinksTraversal = traversed
```

*Which is more verbose but can be nice for learning and figuring out the types.*

---

And same with the other optic:

```haskell
orderedOunces :: Traversal' Order Int
orderedOunces = drinksLens <<< traversed <<< prop (Proxy :: _ "ounces")
```

---

### Using traversals

We can use traversals to **modify** all values (like `map` or `traverse`) or **get all** the values.

---

💡 We can also combine all the values into a single value (like `fold`). But it’s not crucial for now.

---

Let’s modify the order by adding another drink:

```haskell
myOrder =
      { drinks:
          [ { liquor: Scotch, mixer: Soda, ounces: 10 }
          , { liquor: Gin, mixer: Tonic, ounces: 10 }
          ]
      }
```

We can use a traversal (from `Order` to `Liquor`) to **modify** all values using the `over` function as we did with a lens; for example, to bump all the drink sizes in the order. **We don’t even have to change the code:**

```haskell
over orderedOunces (_ + 1) myOrder
-- { drinks: [{ liquor: Scotch, mixer: Soda, ounces: 11 },{ liquor: Gin, mixer: Tonic, ounces: 11 }] }
```

We can also use a traversal to **get all the values** using `toListOf` (or `toArrayOf`):

```haskell
toArrayOf orderedOunces myOrder
-- [10,10]

toListOf orderedLiquor myOrder
-- (Scotch : Gin : Nil)
```

### Quick recap

- A traversal focuses on 0, 1, or many values of the structure (or collection).
- We can compose lenses and traversals to get traversals.
- We can use the `traversed` function to create a traversal.
- We can use `over` to **modify** values with a traversal and `toList` to **get the list** of values.

---

💡 Cheat sheets and references:

![Overview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w6cm8jyojr7yk9u6ot1t.png)

- [Optics cheat sheets](https://impurepics.com/posts/2020-03-22-optics.html)
- [Practical PureScript optics](https://thomashoneyman.com/articles/practical-profunctor-lenses-optics/)

---
{% embed https://youtu.be/tIUXB0TrlpU %}