Cargo Culting Lenses for Fun & Profit

QFPL @ Data61, CSIRO

sean.chalmers@data61.csiro.au

The Point

  • To really use lenses, you will need to learn the theory.
  • However, you don't need to learn the theory to start using lenses.
  • I hope to give you that start on the rote application of lenses.
  • I am no expert

Sacks of Lenses

  • I'll be using Haskell lens package.
  • OCaml has lenses, ocaml-optics, ocaml-lens.
  • Scala too, monocle.

Lens ~ Getter & Setter

-- Getter
(^.) :: s -> Getting a s a -> a
-- Setter
(.~) :: ASetter s t a b -> b -> s -> t

Talk over, right?

Simplest Simples

data Foo = Foo
  { _petCamelName     :: Text
  , _petCamelTopSpeed :: Int
  }
-- Template Haskell to write the lenses for us
makeLenses ''Foo

Getter

_petCamelName foo = "Fred" = foo ^. petCamelName

Yay?

[Inception Movie Pun]

Reaching deeper into a data structure:

data Bar = Bar { _camelPerson :: Foo }

"Sally" = bar ^. camelPerson . petCamelName
--
"Sally" = _petCamelName . _camelPerson $ bar

Lenses are functions

Lenses compose to make new lenses:

camelPerson :: Lens' Bar Foo
petCamelName :: Lens' Foo Text

Compose these with (.)

camelPerson . petCamelName :: Lens' Bar Text

Setter

Simple replacement using (.~)

(petCamelName .~) :: Text -> Foo -> Foo
--
a & petCamelName .~ "Sally" = a { _petCamelName = "Sally" }

Using reverse apply (&):

foo & petCamelName .~ "Sally"

What about updating?

What about updating a thing in a thing in a thing?

nestedUpdate a =
  let
    nn = _petCamelName . _camelPerson $ a
  in
    a { _camelPerson =
        _camelPerson a {
          _petCamelName = nn <> " the Wise"
        }
      }

Another time, another place

Helper.returnNullOrCallFunction(
        Helper.returnNullOrCallFunction(
                myObject.getSomeOtherObject(),
                SomeOtherObject::getAnotherObject
        ),
        AnotherObject::increaseTheThing
);

Taken from: https://stackoverflow.com/a/26414202

But using lenses

nestedUpdate =
  -- Using `set with function`
  camelPerson . petCamelName %~ (<> " the Wise")
  -- Using a mappend setter
  camelPerson . petCamelname <>~ " the Wise"

Multiple Updates

foo & petCamelName .~ "Sally"
    & petCamelTopSpeed .~ 48
--
camelPerson %~ \f -> f
  & petCamelName .~ "Sally" 
  & petCamelTopSpeed .~ 48

Update with a function

Apply a function to your lens target

petCamelTopSpeed %~ (+10)

There are heaps of prebaked operators:

petCamelTopSpeed +~ 10           -- Add 10 cabbages
petCamelTopSpeed //~ 2           -- Divide by 2 cabbages
petCamelName     <>~ " the Wise" -- Reward victory with a title

Getting interesting

Geddit? eh? eh? ...fine.

data A = A { _bars :: [Bar] }
  • Get the names of all the camels
  • Update speed values
  • What function could possibly achieve such a thing?

traverse <3

  • tBars = bars . traverse
  • -- (^..) is an alias of 'toListOf'
    a ^.. tBars . camelPerson . petCamelName
  • -- Works with setters too, of course
    tBars . camelPerson . petCamelTopSpeed +~ 10
  • -- Can we do this? What is the answer?
    a ^. tBars . camelPerson . petCamelName

Traversal

tBars :: Traversal' A Bar
tBars = bars . traverse

Which we can extend and reuse!

topSpeeds :: Traversal' A Int
topSpeeds = tBars . camelPerson . petCamelTopSpeed

Exercise: Write the same function without lens

topSpeeds :: Applicative f => (Int -> f Int) -> A -> f A

Fold

Composing a Getter with a Traversal will yield a Fold.

-- We saw a Fold earlier : (^..)
a ^.. tBars . camelPerson . petCamelName
-- Only want the first camel person?
a ^? bars . _head :: Maybe Bar

Lens Family Tree

  Fold   Setter
  /  \_____/
  \ /      \
Getter  Traversal
   /  ____/
   \ /    \
  Lens   Prism
     \   /
      Iso

Lets get wild

Update a value in a StateT?

-- Assuming : StateT Foo m
petCamelName     .= "Bub"         -- Replace
petCamelTopSpeed %= (-10)         -- Map
petCamelName     <>= " the Swift" -- Mappend
  • What if it's in a thing in the thing in StateT?
  • -- Assuming : StateT Bar m
    camelPerson . petCamelTopSpeed %= (-10)

Giant Updates

Update several different fields on a record.

\n ds ->
  ds & dataSet_max %~ max n
     & dataSet_min %~ min n
     & dataSet_lines . traverse %~
     ( ( line_end . _Point . _1 +~ stepToTheRight )
     . ( line_start . _Point . _1 +~ stepToTheRight )
     )
     & dataSet_lines %~ (\xs -> addNewDataPoint n xs (uncons xs))

The Heck was _Point / _1

That was a Prism.

  • Prisms are Traversals that may become Getters.
  • One way to think of Prisms is as a Lens that is partial in one direction, with laws.
  • Right 'c' ^? _Left == Nothing
    Left 'c' ^? _Left == Just 'c'
  • 5 ^. re _Left == Left 5
  • ( ( Left 5 ) & _Left .~ 6 ) == Left 6

Handling Failure

Consider the following JSON:

{ "alpha":
  { "beta":
    [.., { "gamma": [.., {"delta": <some bool> }, ...] }, ..]
  }
}
  • Assuming gamma is at index 3, and delta is at index 2. > Your mission is to flip the value at delta.

First Hack

a = getAtKey blob "alpha"
if ( null != a ) {
  b = getAtKey a "beta" {
    if ( null != b && isArray b ) {
      c = getAtIndex b 3
      if ( null != c ) {
        fooList = getAtKey "gamma"
        if ( null != fooList && isArray fooList ) {
          fooObj = getAtIndex c 2
          if ( null != fooObj ) {
            fooVal = getBoolValAtKey "delta" fooObj
            if ( null != fooVal ) {
              newFoo = setValueAtKey "delta" (not fooVal)
              return ( setValueAtKey "beta" a
                ( setValueAtIndex 3 b
                  ( setValueAtKey  "gamma" c
                    ( setValueAtIndex 2 fooList newFoo )
                  )
                )
              )
            }
          }
        }
      }
    }
  }
}

This pseudo-code is an exaggeration, but not by much and you know it.

Yay

Using lens-aeson and lens.

( key "alpha"
. key "beta" . _Array . ix 3
. key "gamma" . _Array . ix 2
. key "delta" . _Bool %~ not
) :: AsValue t => t -> t

ahem

Prisms

The _Array and _Bool things are both a Prism.

"[1,2,3]" ^? _Array
--
[1,2,3] ^. re _Array

Prisms are Traversals

Just 3 ^? _Just = Just 3
Nothing ^? _Just = Nothing
('a', Just 3) & _2 . _Just %~ (+1) = ('a', Just 4)

Lens Operator Grammar

Lens operators are formed from a symbol DSL.

  • ^ - refers to a fold
  • ~ - modification or setting
  • ? - results optional
  • < - return the new value
  • << - return the old value
  • % - use a given function
  • %% - use a given traversal
  • = - apply lens to MonadState
  • . - which side should have the lenses

Modifying a Map

let
  m = Map.fromList [(1, "susan"), (2, "jo")]
in
  m & at 1 .~ "bob"
    & at 3 ?~ "pixie"
    & at 2 %~ _f :: ??

Oh my

Modifying a map in your StateT, setting a new value if one exists, but creating a new key-value pair if it doesn't exist, and returning the previous value if there was one?

data A = A
  { _theFoo :: Foo
  , _theMap :: Map String Int
  }
  • a <- theMap . at "foo" <<?= 37

The 'Of' family

Use a lens to target a part of a structure.

  • traverseOf, itraverseOf
  • maximumOf, minimumOf
  • foldMapOf, mapMOf

How I learned

  • Haddock diving
  • Playing in the REPL
  • "Lets Lens" introductory course

Trying it yourself

REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL REPL

Thanks for suffering through

Questions ?

Lets Lens! - https://github.com/data61/lets-lens