Skip to content

Instantly share code, notes, and snippets.

@michaelavila
Created July 11, 2017 17:15
Show Gist options
  • Save michaelavila/7f8d647a591181006c6a090dd43f0b9c to your computer and use it in GitHub Desktop.
Save michaelavila/7f8d647a591181006c6a090dd43f0b9c to your computer and use it in GitHub Desktop.
Control.Lens Notes

Lens

Control.Lens helps you to get, set, and modify the particulars of your data. The ideas behind lens are interesting for a number of reasons, not the least of which is composability. Because of this, Simon Peyton Jones titled his talk on lens "Compositional Data Access and Manipulation", which is a great title. You should watch that talk.

The agenda is:

  1. Peek at lens
  2. Work through the Lens type
  3. Chat

What Using Control.Lens Looks Like

Suppose we have the following types:

type Radius = Double

type X = Double
type Y = Double
type Point = (X,Y)

data Circle = Circle { center :: Point
                     , radius :: Radius } deriving Show

circle :: Circle
circle = Circle (10,5) 100

Here are some lenses for these types (don't expect to understand these yet):

import Control.Lens

_center :: Lens' Circle Point
_center g c = (setCenter c) <$> g (getCenter c)
  where getCenter = center
        setCenter c p = c { center = p }

_x :: Lens' Point X
_x g (x,y) = (,y) <$> g x
        
_y :: Lens' Point Y
_y g (x,y) = (x,) <$> g y

Note _center can be created using a makeLenses helper, but I've included it here to illustrate. Same for _x and _y, you can use _1 and _2.

These lenses can be used to access and manipulate the position of the circle like this:

> view (_center . _x) circle
10.0
>
> set (_center . _y) 20 circle
Circle {center = (10.0,20.0), radius = 100.0}
>
> over (_center . _x) (+5) circle
Circle {center = (15.0,5.0), radius = 100.0}

In what might be called idiomatic lens, the expressions above can be rewritten as:

> circle ^. (_center . _x)
10.0
>
> (_center . _y) .~ 20 $ circle
Circle {center = (10.0,20.0), radius = 100.0}
>
> (_center . _x) %~ (+5) $ circle
Circle {center = (15.0,5.0), radius = 100.0}

It's worth noting:

  • a lens can be created from a getter and setter pair
  • a lens can be created without a getter and setter pair
  • get and set can be recovered from a lens
  • access and manipulation of the data does not break immutability
  • lenses can be composed
  • access and modify kind of look like dot syntax from OO-languages:
    • e.g. circle^._center._y accesses the y component of the center of a circle
  • lens operations have operators, which can be found at Operators

The fact that a Lens can be created from a getter and setter pair is codified in the lens function of the Control.Lens package, which can be used to simplify the above _center function to:

_center :: Lens' Circle Point
_center = lens center (\c p -> c { center = p })

Lenses can be created for aspects of data you can't see in the source code directly, which is kind of interesting:

_radius :: Lens' Circle Radius
_radius = lens radius (\c r -> c { radius = r })
        
type Area = Double
_area :: Lens' Radius Area
_area = lens (\r -> pi*r^2) (\_ a -> sqrt (a/pi))

Note I believe that _area could be an Iso since you can go from area to radius and back without losing any information.

Which can be used to access and modify the area of the circle, notice how the radius changes:

> (_radius . _area) %~ (*2) $ circle
Circle {center = (10.0,5.0), radius = 141.4213562373095}

Lenses can even take advantage of things like IO:

> import System.Random
> (_radius . _area) (const randomIO) circle
Circle {center = (10.0,5.0), radius = 0.31640692613569155}

So, how does this all work?

The Lens Type

There are many types introduced and used by Control.Lens. Armed with an understanding of the Lens type you should have no problem figuring out the many other types used. Here's Lens:

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t

Stop Think about this type.

So Lens is just a type of function. Simple enough.

Note forall f. exists here because the Lens type is a function that could exist as part of another function. When there is more than one type constraint in a type declaration, the type is considered a RankNType where N is the number of foralls. So forall exists here to keep the type constraint on f scoped to just the definition of the Lens type in the event that Lens becomes part of a RankNType. Don't think about it too much right now.

Here's how you might come up with lens yourself. Let's start with the Circle types again:

type Radius = Double

type X = Double
type Y = Double
type Point = (X,Y)

data Circle = Circle { center :: Point
                     , radius :: Radius } deriving Show

circle :: Circle
circle = Circle (10,5) 100

First, how would we get, set, and modify center and radius?

> center circle
(10.0,5.0)
>
> radius circle
100.0
>
> circle { center = (1,2) }
Circle {center = (1.0,2.0), radius = 100.0 }
>
> circle { radius = 5 }
Circle {center = (10.0,5.0), radius = 5.0}

We can even compose getters:

> (fst . center) circle
10.0

Setters don't compose as nicely. What about modify? A straightforward approach would be to do a get, then modify the value, then do a set. Here's what that looks like:

> let modifyRadius = \c g -> (\r -> c { radius = r }) (g (radius c))
>
> modifyRadius circle (+10)
Circle {center = (10.0,5.0), radius = 110.0}

Since we can modify in this way given any getter and setter pair, let's make a general modify function:

modify :: (s -> a) -> (s -> b -> t) -> (a -> b) -> s -> t
modify get set g s = (set s) (g (get s))

Now we can create modify... functions given any getter and setter pair:

> let modifyRadius = modify radius (\c r -> c { radius = r })
> modifyRadius (+10) circle
Circle {center = (10.0,5.0), radius = 110.0}

What about nested data?

modifyCenter :: (Point -> Point) -> Circle -> Circle
modifyCenter = modify center (\c p -> c { center = p })

modifyX :: (X -> X) -> Point -> Point
modifyX = modify fst (\(_,y) x -> (x,y))

> modifyCenter (modifyX (+10)) circle
Circle {center = (20.0,5.0), radius = 100.0}

If we stare at the types of modifyCenter and modifyX long enough we might notice that they are composable:

modifyX                :: (X -> X) -> Point -> Point
modifyCenter           ::            (Point -> Point) -> Circle -> Circle

modifyCenter . modifyX :: (X -> X) -> Circle -> Circle

> (modifyCenter . modifyX) (+9) circle
Circle {center = (19.0,5.0), radius = 100.0}

The result of composing modifyCenter with modifyX is a function that will modify the x part of the center point of a circle. This is the gist of why lenses are composable. Neat.

As is always the case, this composability is interesting because we can combine these lenses in different ways. Here we use modifyCenter with another lens to produce different behavior:

modifyY :: (Y -> Y) -> Point -> Point
modifyY = modify snd (\(x,_) y -> (x,y))

> (modifyCenter . modifyY) (+13) circle
Circle {center = (10.0,18.0), radius = 100.0}

So these modify functions, which have the type (a -> b) -> s -> t, can be composed with one another to get, set, and modify our structured data. Let's capture this in a type and make our first connection to Control.Lens:

type Lens s t a b = (a -> b) -> s -> t
type Lens' s a = Lens s s a a

We can change the type signatures for our modify functions to:

modify       :: (s -> a) -> (s -> b -> t) -> Lens s t a b
modifyCenter :: Lens' Circle Point
modifyX      :: Lens' Point X
modifyY      :: Lens' Point Y

Recovering Get and Set

Given our current definition of Lens there's no general way to do things like get, but we can do set. Here's set:

> modifyY (const 3) (1,2)
(1.0,3.0)

Which makes sense. If we give the const 3 function as the modifier function then no matter what the y component is set to 3.0. In order to recover get we need to modify our Lens type. We could return a tuple that contained the value before we modified it, like:

type Lens s t a b = (a -> b) -> s -> (a,t)

But this seems clunky. Rather than always wrap the result up in a tuple, let's allow the caller to choose how to wrap by introducing Functor. This turns out to be much more powerful:

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t

Now that the Lens type has changed we need to update the definition of modify:

modify get set g s = (set s) <$> (g (get s))

With Functor in place we can get value from a lens like so:

> import Control.Applicative (Const)
> getConst $ modifyCenter (\x -> Const x) circle
(10.0,5.0)

The trick here is knowing that given any a and b, Const will always unwrap to an a. So wrapping something in Const, then operating on that Const, then unwrapping that Const always results with what you put into it.

> :t Const
Const :: a -> Const a b
> :t (,)
(,) :: a -> b -> (a, b)

Which we could call view:

view :: Lens s t a b -> s -> a
view s = getConst $ modifyCenter (\x -> Const x) s

> view modifyCenter circle
(10.0,5.0)

Set behavior can implemented in a manner similar to get, but using the Identity functor instead. So now we've recovered both get and set, but we had to modify the Lens type. Notice though, that the last modification caused our Lens type to match exactly the Lens type provided by Control.Lens.

It turns out that Functor is the secret sauce, so to speak, of Control.Lens. SPJ calls this Edward's "big insight".

Now we can create a Lens without a getter and setter pair, but still get and set. Recall _y from earlier, it was not implemented from a getter and setter pair, but we can get and set with it:

_y :: Lens' Point Y
_y g (x,y) = (x,) <$> g y

> view (_center . _y) circle
5.0
>
> set (_center . _y) 20 circle
Circle {center = (10.0,20.0), radius = 100.0}

What's next ...

This should be enough to prepare you for the rest of Control.Lens, namely Traversable, Foldable, Iso, Prism, Getting, Setting, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment