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, Maybe
s, 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.
.
go deeper / compose (normal composition operator)
^.
get / lookup
^?
get nullable field (see Nullable
below)
&
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
^?
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.
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:
data Person = Person{ _name :: Text, _address :: Address, _pet :: Pet}​data Pet = Pet{ _name :: Text, _variety :: Animal, _favouriteToy :: Toy}​data Animal = Dog | Cat | Fish​data Toy = Toy{ _ name :: Text, _condition :: Condition}​data Condition = New | Good | Fair | Bad
Let's define an owner :: Person
:
owner = Person{ _name = "Alice", _address = "123 Fake Street", _pet = Pet{ _name = "Fluffy", _variety = Cat, _favouriteToy = Toy{ _name = "wind-up mouse", _condition = New}}}
Manually updating deeply nested records is a bit ugly
wearAndTear :: PersonwearAndTear = owner { _pet = myPet { _favouriteToy = myToy { _condition = Fair } } }wherePerson { _pet = myPet@(Pet { _favouriteToy = myToy })} = owner
Lenses read better
wearAndTear :: PersonwearAndTear = owner & pet . favouriteToy . condition .~ Fair
Let's break that down 🕺
-- Pipe Replace-- | |-- v vwearAndTear = owner & pet . favouriteToy . condition .~ Fair-- ^ ^ ^ ^ ^-- | | | | |-- | +---------+-----------+ Replace with-- Initial Value |-- Nested path-- (like in OO dot-notation)-- owner.pet.favouriteToy.condition
These can also be turned into helper functions, chained together, and so on
playWithPet :: Person -> PersonplayWithPet = pet . favouriteToy . condition .~ Fair​-- Use​playWithPet owner​-- Oh no! Fluffy ran away 😠Let's play with our new cat: Mittens!​owner & pet.name.~"Mittens"& playWithPet​-- And exciting! We moved, and got a dog to play with​owner & address .~ "1066 West Hastings Street"& pet.variety .~ Dog& pet.name .~ "Lassie"& pet.toy.name .~ "Tennis ball"& playWithPet
We can also automate some behaviour while we're at it:
playWithPet' :: Person -> PersonplayWithPet' = pet . favouriteToy . condition %~ wearDown​wearDown :: Condition -> ConditionwearDown New = GoodwearDown Good = FairwearDown Fair = BadwearDown Bad = Bad
​