Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Basic Optics: Lenses, Prisms, and Traversals

Basic Optics: Lenses, Prisms, and Traversals

Lenses are becoming an increasingly important part of a Haskeller’s toolkit. Yet, when first approaching them, people may feel buried under a myriad of different lens-like thingies, and the complexity of some of the libraries implementing those concepts, like lens.

The goal of this talk is to provide a conceptual overview of three of the most important kinds of optics, namely lenses, prisms, and traversals. For most data types, those optics can be automatically generated, something we shall discuss. Finally, we shall look at one useful application of optics: treating semi-structured data such as JSON documents.

This presentation given by Alejandro Serrano is part of the 47 Degrees Academy. The full video of this talk can be found here: https://www.youtube.com/watch?v=geV8F59q48E

47 Degrees Academy

June 16, 2020
Tweet

More Decks by 47 Degrees Academy

Other Decks in Programming

Transcript

  1. Optics? A composable way to access parts of a value

    optic composition │ ↓ person ˆ. name % firstName ↑ ↑ ↑ ↑ │ │ └ optics ┘ value │ operation We build new optics by composition Di erent optics enable di erent operations
  2. Goals of this talk 1. The concepts behind optics. 2.

    The practicalities of using optics. Non-goal: how to implement optics You might have heard about these: direct representation, van Laarhoven representation, profunctor optics.
  3. What this talk is about 1. The concepts behind optics.

    2. The practicalities of using optics. Library of choice: Simpler interface, good error messages. lens, microlens, lens-family take other routes (more power, smaller, ...). optics
  4. Lens = getter + setter Get the value of the

    name in p Set a new value for the name in p data Person = Person Name Age view name p {- or -} p ^. name set name "B" p {- or -} p & name .~ "B" view + apply function + set over name ("Mr . " <>) p {- or -} p & name %~ ("Mr . " <>)
  5. Lenses: rst-class properties A lens is a value on its

    own name :: Lens' Person Name firstName :: Lens' Name String lastName :: Lens' Name String Composition gives back a new lens name % firstName :: Lens' Person String
  6. Lens composition view (l1 % l2) v = view l2

    (view l1 v) = (v ^. l1) ^. l2 set (l1 % l2) new v = let oldL1 = view l1 v newL1 = set l2 new oldL1 in set l1 newL1 v 1 2 3 4 5 6 7 8 view (l1 % l2) v = view l2 (view l1 v) = (v ^. l1) ^. l2 1 2 3 4 set (l1 % l2) new v 5 = let oldL1 = view l1 v 6 newL1 = set l2 new oldL1 7 in set l1 newL1 v 8 = view l2 (view l1 v) view (l1 % l2) v 1 2 = (v ^. l1) ^. l2 3 4 set (l1 % l2) new v 5 = let oldL1 = view l1 v 6 newL1 = set l2 new oldL1 7 in set l1 newL1 v 8 = (v ^. l1) ^. l2 view (l1 % l2) v 1 = view l2 (view l1 v) 2 3 4 set (l1 % l2) new v 5 = let oldL1 = view l1 v 6 newL1 = set l2 new oldL1 7 in set l1 newL1 v 8 set (l1 % l2) new v = let oldL1 = view l1 v newL1 = set l2 new oldL1 in set l1 newL1 v view (l1 % l2) v 1 = view l2 (view l1 v) 2 = (v ^. l1) ^. l2 3 4 5 6 7 8 = let oldL1 = view l1 v view (l1 % l2) v 1 = view l2 (view l1 v) 2 = (v ^. l1) ^. l2 3 4 set (l1 % l2) new v 5 6 newL1 = set l2 new oldL1 7 in set l1 newL1 v 8 newL1 = set l2 new oldL1 view (l1 % l2) v 1 = view l2 (view l1 v) 2 = (v ^. l1) ^. l2 3 4 set (l1 % l2) new v 5 = let oldL1 = view l1 v 6 7 in set l1 newL1 v 8 = let oldL1 = view l1 v newL1 = set l2 new oldL1 in set l1 newL1 v view (l1 % l2) v 1 = view l2 (view l1 v) 2 = (v ^. l1) ^. l2 3 4 set (l1 % l2) new v 5 6 7 8
  7. Lenses: rst-class properties p.name.firstName = (p.name).firstName p ^. name %

    firstName = p ^. (name % firstName) 1. Access the name property ⇒ obtains a value n 2. Access the firstName property on n, i.e., n.firstName 1. Create a composite lens l = name % firstName
  8. Optics-based interface Instead of record elds, expose only optics You

    may expose elds using di erent optics Virtual elds are not backed up by any value You can modify the underlying structure without risk of breakage p & firstLastName .~ "A, B"
  9. Optics-based interface, bonus Modifying nested records in Haskell is a

    pain person.name.firstName = "Pepe" // Java p { name = name p { firstName = "Pepe" }} -- Haskell p & name % firstName .~ "Pepe" -- Haskell + optics
  10. Where do lenses come from? You can write them by

    hand lens :: (s -> a) -> (s -> a -> s) -> Lens' s a -- ↑ ↑ -- getter setter data Person = Person { _name :: Name, ... } name = lens _name (\p new = p { _name = p }) Automate this using Template Haskell {-# language TemplateHaskell #-} -- top of the module data Person = Person { _name :: Name, ... } makeLenses ''Person -- '_field' creates 'field' lens
  11. Lenses: summary 1. Lenses o er two main operations: view

    or (^.) set or (.~) 2. Lenses are values on their own New lenses are built by (%) Questions? What about de ning lenses for User? data User = Person { _name :: Name, _age :: Age } | Entity { _legalName :: Name }
  12. A ne traversals Take a type with multiple constructors data

    User = Person { _name :: Name, _age :: Age } | Entity { _legalName :: Name } Or with our shape-like friends data Value = { one :: ▱, other :: ◦ } | { this :: △, that :: ◦ }
  13. A ne traversals Gets the value pointed by the a

    ne traversal This information may not be there Sets the new value if there was one In this case, do nothing for entities data User = Person { _name :: Name, _age :: Age } | Entity { _legalName :: Name } preview age user {- or -} user ^? age set age 30 user {- or -} set age 30 user
  14. A ne traversals and lenses What happens when we compose

    these? data User = Person { _name :: Name, _age :: Age } | Entity { _legalName :: Name } makeLenses ''User -- knows when to generate each one name :: AffineTraversal' User Name firstName :: Lens' Name String 1. The value may not be there 2. If it is there, we can update it name % firstName :: AffineTraversal' User String
  15. Prisms data Event = UserAdded User | OrderShipped Order makePrisms

    ''Event Some operations remain as before preview _UserAdded e -- as before set _UserAdded u t -- as before Builds a new Event review _UserAdded u {- or -} _UserAdded # u
  16. Prisms If your constructor has one eld (or none), you

    can build it from that piece of data data Event = UserAdded User | OrderShipped Order makePrisms ''Event
  17. Why bother? data User = Person { _name :: Name

    , _age :: Age } | Entity { _legalName :: Name } makeLenses ''User data User = Person PersonData | Entity EntityData makePrisms ''User data PersonData = PD { _name :: Name, _age :: data EntityData = ED { _legalName :: Name } mapM makeLenses [''PersonData, ' name :: AffineTraversal' User Name _Person :: Prism' User Pe name :: Lens' PersonD _Person % name :: AffineTraversa You can split a data type in prisms + lenses
  18. First-class everything! With lenses you can modify the underlying structure

    while exposing the same elds Prisms do the same for constructors And a ne traversals for pattern matching! getName :: User -> String getName u | Just pd <- u ^? Person = pd ^. name % firstName
  19. Pattern guards Matches support Boolean conditions wonderfulFunction x | check

    x = ... -- do something You also have the ability to match after applying a function wonderfulFunction x | Just v <- check x = ... getName :: User -> String getName u | Just pd <- u ^? _Person = pd ^. name % firstName | Just ed <- u ^? _Entity = ed ^. legalName
  20. A ne traversals and prisms: summary 1. A ne traversals

    o er two main operations: preview or (^?) set or (.~) 2. Prisms add an additional feature review or (#) Optics form a hierarchy under (%)
  21. Values with many parts Elements on a list Values in

    a dictionary Keys in a JSON value ...
  22. Values with many parts (Non-a ne) traversals Treat data as

    a bulk Indices Target speci c values
  23. (Non-a ne) traversals The structure may have any amount of

    parts Very powerful in combination with other optics > over each (+1) [1, 2, 3] [2, 3, 4] over (each % _Person % age) (+1) [...]
  24. (Non-a ne) traversals The structure may have any amount of

    parts Very powerful in combination with other optics This is hard to express without this framework! > over each (+1) [1, 2, 3] [2, 3, 4] over (each % _Person % age) (+1) [...]
  25. Obtaining traversals traversed Works on any Traversable data type. Most

    containers, like lists, maps, or trees. each Type class speci cally designed for traversals. Monomorphic containers: tuples, IntSet...
  26. Updating vs. setting Using traversals, over is more useful than

    set lst & each .~ 1 lst & each %~ (+1) set changes every element to the same value over takes into account the previous value
  27. aeson-optics Values can be primitives, arrays, or objects _Primitive ::

    Prism' Value Primitive _Object :: Prism' Value (HashMap Text Value) _Array :: Prism' Value (Vector Value) Traversals to access keys and elements members :: IxTraversal' Text Value Value values :: IxTraversal' Int Value Value
  28. GitHub's REST API Blog Forum The content on this site

    may be out of date. For the most accurate and up-to-date content, visit The content on this site may be out of date. For the most accurate and up-to-date content, visit docs.github.com/v3/repos/#response-5 docs.github.com/v3/repos/#response-5. . We've unified all of GitHub's product documentation in one place! Check out the new locations for We've unified all of GitHub's product documentation in one place! Check out the new locations for REST API REST API, , GraphQL API GraphQL API,, and and Developers Developers. . Learn more on the Learn more on the GitHub blog GitHub blog.. Docs Versions Search… REST API v3 Reference Guides Libraries GET http://github.com/repos/serras/my-repo
  29. Optics to retrieve keys ix, key :: Text -> AffineTraversal'

    Value Value ix, nth :: Int -> AffineTraversal' Value Value Get all the topics, which ought to be strings result ^. key "topics" % values % _String Find whether the user has push permission result ^. key "permissions" % key "push" % _Bool
  30. Using indices ix and at access the value at some

    index ix :: index -> AffineTraversal' container element at :: index -> Lens' container (Maybe element) ix never modi es the structure dict & ix "alex" % _Person % age %~ (+1) at allows to add or remove elements dict & ix "alex" % _Person .~ Just (PD ...)
  31. Indexed optics There is a generalized version of (almost) every

    optic to use with indices -- indexed lenses and traversals iview :: IxOptic s a -> s -> (i, a) ipreview :: IxOptic s a -> s -> Maybe (i, a) iset :: IxOptic s a -> (i -> a) -> s -> s Obtaining indexed optics Many optics are overloaded for both versions > iover each (+) [1, 2, 3] [1, 3, 5]
  32. Composing indexed optics Non-indexed Indexed Non-indexed (%) (%) Indexed (%)

    (<%>) Indexed + indexed optic The new index is a tuple of the old ones
  33. A question to the audience What kind of optic is

    a lens and a prism at the same time?
  34. A lens and a prism? Lens: getting a value never

    fails There can be only one constructor Prism: we can build a value from that part There can be only one eld The value and the part must be equivalent We have here an isomorphism Iso Age Int newtype Age = Age Int
  35. Optics are a composable (with %) way to access or

    modify parts of a value Lens: exactly 1, rst-class elds A ne traversals: 0 or 1, rst-class matching Prism: build a value from the part, rst-class constructor Traversal: any amount of that part data Person = Person { name :: Name, age :: Int } data Person = Person { name :: Name, age :: Int } | Entity { ... } data Person = Person PersonData | Entity EntityData data People = People [Person]