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:
- Peek at lens
- Work through the
Lens
type - Chat
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 amakeLenses
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 they
component of the center of a circle
- e.g.
- 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 anIso
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?
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 theLens
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 aRankNType
where N is the number offorall
s. Soforall
exists here to keep the type constraint onf
scoped to just the definition of theLens
type in the event thatLens
becomes part of aRankNType
. 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
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}
This should be enough to prepare you for the rest of Control.Lens
, namely
Traversable
, Foldable
, Iso
, Prism
, Getting
, Setting
, etc.