Appendix II: Optics
Common Lens Legend
My guess is that you're seeing lenses and going "what are all these funky looking operators? 😱" In short, they're getters and setters that let you work on nested data, enums, Maybes, and so on. They compose nicely, so you can all kinds of mutable-feeling updates in a totally controlled way.
The good news is that once you know the lens library, it's used in a TON of places, so it's not wasted effort. You can really get by with knowing a handful of them, and following the patterns.
The idea is that you're zooming into a data structure. You can grab values out, or edit them. Then you zoom back out and the outer structures have also been updated to reflect point at the inner changes.

Getters ^

    . go deeper / compose (normal composition operator)
    ^. get / lookup
    ^? get nullable field (see Nullable below)

Setters ~

    & mutate with / and also (it's the normal pipe / reverse application operator)
    .~ set / replace
    ?~ set a nullable field (see Nullable below)
    %~ update with a function (run function on current value and set it to that)
    +~ add to the current value (e.g. counter)
    -~ subtract from current value
    *~ multiple current value by

Nullable ?

    ^? get inside a Maybe (i.e. stop lensing if it's a Nothing)
    ?~ set a nullable field (i.e. .~ Just newValue)
These read nicely, and let you make deeply nested updates either with composition (.) or with a named lens that is a composition of others.

Why not use record-update syntax?

You totally can! Doing this manually, you end up having to update each nested record recursively, including the updated sub-records as you go. For example:
1
data Person = Person
2
{ _name :: Text
3
, _address :: Address
4
, _pet :: Pet
5
}
6
​
7
data Pet = Pet
8
{ _name :: Text
9
, _variety :: Animal
10
, _favouriteToy :: Toy
11
}
12
​
13
data Animal = Dog | Cat | Fish
14
​
15
data Toy = Toy
16
{ _ name :: Text
17
, _condition :: Condition
18
}
19
​
20
data Condition = New | Good | Fair | Bad
Copied!
Let's define an owner :: Person:
1
owner = Person
2
{ _name = "Alice"
3
, _address = "123 Fake Street"
4
, _pet = Pet
5
{ _name = "Fluffy"
6
, _variety = Cat
7
, _favouriteToy = Toy
8
{ _name = "wind-up mouse"
9
, _condition = New
10
}
11
}
12
}
Copied!
Manually updating deeply nested records is a bit ugly
1
wearAndTear :: Person
2
wearAndTear = owner { _pet = myPet { _favouriteToy = myToy { _condition = Fair } } }
3
where
4
Person { _pet = myPet@(Pet { _favouriteToy = myToy })} = owner
Copied!
Lenses read better
1
wearAndTear :: Person
2
wearAndTear = owner & pet . favouriteToy . condition .~ Fair
Copied!
Let's break that down πŸ•Ί
1
-- Pipe Replace
2
-- | |
3
-- v v
4
wearAndTear = owner & pet . favouriteToy . condition .~ Fair
5
-- ^ ^ ^ ^ ^
6
-- | | | | |
7
-- | +---------+-----------+ Replace with
8
-- Initial Value |
9
-- Nested path
10
-- (like in OO dot-notation)
11
-- owner.pet.favouriteToy.condition
Copied!
These can also be turned into helper functions, chained together, and so on
1
playWithPet :: Person -> Person
2
playWithPet = pet . favouriteToy . condition .~ Fair
3
​
4
-- Use
5
​
6
playWithPet owner
7
​
8
-- Oh no! Fluffy ran away 😭 Let's play with our new cat: Mittens!
9
​
10
owner & pet.name.~"Mittens"
11
& playWithPet
12
​
13
-- And exciting! We moved, and got a dog to play with
14
​
15
owner & address .~ "1066 West Hastings Street"
16
& pet.variety .~ Dog
17
& pet.name .~ "Lassie"
18
& pet.toy.name .~ "Tennis ball"
19
& playWithPet
Copied!
We can also automate some behaviour while we're at it:
1
playWithPet' :: Person -> Person
2
playWithPet' = pet . favouriteToy . condition %~ wearDown
3
​
4
wearDown :: Condition -> Condition
5
wearDown New = Good
6
wearDown Good = Fair
7
wearDown Fair = Bad
8
wearDown Bad = Bad
Copied!
​
Last modified 1yr ago
Export as PDF
Copy link