Lenses

Today’s lecture introduces lenses, and is inspired by the lens-tutorial.

The lens abstraction, and related abstractions, make the concept of a field of an abstraction, a first class notion. It is a little language of its own, uses nice type trickery, and certainly has a learning curve. But when well-understood, it allows for concice, expressive code, and opens new ways of abstraction. It is worth considering in every medium-to-large sized project that handles deep structured data. It is also worth learning because some interesting libraries, such as diagrams, make heavy use of it.

# Getters and Setters

Here is a product type with a bunch of fields:

data Atom = Atom { _element :: String, _point :: Point }
data Point = Point { _x :: Double, _y :: Double }

## Getters

Haskell’s record syntax makes it rather easy to reach deeply inside such a data structure. For example, if we want to get the x-position of an atom, we can write

getAtomX :: Atom -> Double
getAtomX = _x . _point

So the record accessors serve as getters, and if we want to reach deeply into a data structure, we can compose these getters. Of course, this is just syntactic sugar, and if we would not have used record syntax, we could easily implement _x and _point by hand.

## Setters

Setting a value is not so easy. There is the record-update syntax that allows us to write the following (but again, the record-update is just syntactic sugar, and we could have written the same with regular pattern matching):

setPoint :: Point -> Atom -> Atom
setPoint p a = a { _point = p }
setElement :: String -> Atom -> Atom
setElement e a = a { _element = e }
setX, setY:: Double -> Point -> Point
setX x p = p { _x = x }
setY y p = p { _y = y }

Unfortunately, these setters do not compose well, as we see when we want to write a function that sets the x of an atom:

setAtomX :: Double -> Atom -> Atom
setAtomX x a = setPoint (setX x (_point a)) a

In order to compose setAtomPoint with setPointX, we also need a getter to get the point of the atom!

So it seems that getters and setters are closely related, and we want to bundle them and work with them together.

## A simple lens

So let us create an abstract data type that combines the getter and setter of a field, and let us call that a lens, as it “zooms into” a field:

data Lens a b = { view :: a -> b
, set :: b -> a -> a
}

The implementations are straight forward:

point :: Lens Atom Point
point = Lens _point setPoint
element :: Lens Atom String
element = Lens _element setElement
x, y :: Lens Point Double
x = Lens _x setX
y = Lens _y setY

In order to implement setAtomX, we want to compose two lenses, and we can do that using a general operator:

comp :: Lens a b -> Lens b c -> Lens a c
comp l1 l2 = Lens (view l2 . view l1)
(\c a -> set l1 (set l2 c (view l1 a)) a)

setAtomX :: Double -> Atom -> Atom
setAtomX = set (point comp x)

## Modify

In the code that we have just written, there is a very common pattern: Applying a function to a field. And clearly, we can implement that using a getter and a setter:

over :: Lens a b -> (b -> b) -> (a -> a)
over l f a = set l (f (view l a)) a

So if we want to move an atom to the right, we can simply write

moveAtom :: Atom -> Atom
moveAtom = over (point comp x) (+1)

We can also rewrite comp:

comp :: Lens a b -> Lens b c -> Lens a c
comp l1 l2 = Lens (view l2 . view l1)
(\c -> over l1 (set l2 c))

## Efficiency

Unfortunately, this is not very efficient. Function over uses the lens l twice: Once to get the value, and once again to set it. And since over is used in comp, if we nest our lenses a few layers deep, this gets inefficient very quickly.

How can we fix this? We make over primitive!

data Lens a a = { view :: a -> b
, set :: b -> a -> a
, over :: (b -> b) -> (a -> a)
}

In order to update our existing primitive lenses, we implement a small helper function to derive the over code, instead of writing it by hand every time.

mkLens :: (a -> b) -> (b -> a -> a) -> Lens a b
mkLens view set = Lens view set over
where over a = set (f (view a)) a

point :: Lens Atom Point
point = mkLens _point setPoint
element :: Lens Atom Element
element = mkLens _element setElement
x, y :: Lens Atom Double
x = mkLens _x setX
y = mkLens _y setY

Now the composition operator uses every lens only once. Good!

comp :: Lens a b -> Lens b c -> Lens a c
comp l1 l2 = Lens (view l2 . view l1)
(\c -> over l1 (set l2 c))
(over l1 . over l2)

In fact, with over as the primitive notion, there is not need for set any more, as that can be implemented with over:

set :: Lens a b -> b -> a -> a
set l x = over l (const x)

# Towards van Laarhoven lenses

This is nice, but what if we want to do an effectful update? For example, this code does not typecheck:

askX :: Atom -> IO Atom
askX a = over (point comp x) askUser a
where
askUser :: Double -> IO Double
putStrLn $"Current position is " ++ show x ++ ". New Position?" answer <- getLine return (read answer) ## IO as motivation Of course we could rewrite it again to use view before any IO actions, and set afterwards, but then we would again be traversing the data structure towards the position of interest twice. We can fix this as we did before, by allowing a variant of over that does IO: data Lens a b = { view :: a -> b , over :: (b -> b) -> (a -> a) , overIO :: (b -> IO b) -> (a -> IO a) } mkLens :: (a -> b) -> (b -> a -> a) -> Lens a b mkLens view set = Lens view over overIO where over a = set (f (view a)) a overIO a = do b' <- f (view a) return$ set b' a

comp :: Lens a b -> Lens b c -> Lens a c
comp l1 l2 = Lens (view l2 . view l1)
(\c -> over l1 (set l2 c))
(over l1 . over l2)
(overIO l1 . overIO l2)

Fancy how composition is so simple again!

## Enter the Functor

But clearly, we want to do this trick not just for IO, but for many type constructors. Some of which might not be Monads. So if we look closely at the code for overIO, we see that all we really need is a functor instance. So let us generalize this:

data Lens a b = { view :: a -> b
, over :: (b -> b) -> (a -> a)
, overF :: forall t. Functor t => (b -> t b) -> (a -> t a)
}

mkLens :: (a -> b) -> (b -> a -> a) -> Lens a b
mkLens view set = Lens view over overF
where over a = set (f (view a)) a
overF a = (\b' -> set b' a) <$> f (view a) The forall t says that the function stored in the overF field of a Lens works with any functor t that you want to use it at. ## Getting rid of over But look at how similar the type signatures of overF and over are. If we could somehow make the t go away, they would be identical, and overF would be enough? So we want a type constructor t that is equal to its argument. type I x = x instance Functor I where fmap f x = f x is a good start, but we cannot define a Functor instance for that, so we have to use a newtype to get the identitiy functor: newtype I x = MkI x unI :: I x -> x unI (MkI x) = x instance Functor F where fmap f x = MkI (f (unI x)) With this particular Functor instance, we can derive over from overF and remove it from the Lens type: over :: Lens a b -> (b -> b) -> (a -> a) over l f a = unI$ overF l f' a
where f' b = MkI (f b)

## Getting rid of view

That was nice, we are again down to two primitive operations. Can we do better? Is, maybe, in some way, view also an instance of overF?

If we try to make the type match, from right to left, it might work if t a would somehow be b – then at least we would have a -> b a the end, as desired.

We would somehow have to provide a function b -> t b though, that works in every case. Since we get to pick t, why not make it always b:

newtype C b x = MkC b

unC :: C b x -> b
unC (MkC b) = b

instance Functor C where
fmap f (MkC b) = MkC b

With this constant functor, we can define view in terms of overF:

view :: Lens a b -> a -> b
view l a = unC $overF l MkC a ## Lens is just a type synonym But now Lens has become a product type with only one field. This menas that the type Lens a b is isomporphic to the type forall t. Functor t => (b -> t b) -> (a -> t a). In that case, why bother with the Lens constructor and the overF field name at all? We can get rid of them! type Lens a b = forall t . Functor t => (b -> t b) -> (a -> t a) Interestingly, now comp :: Lens a b -> Lens b c -> Lens a c comp l1 l2 = l1 . l2 so we can get rid of this function as well, and use plain old function composition .! # Traversals Where there is a Functor, an Applicative cannot be far. What if we do change the constraint in the lens type: type Traversal a b = forall t . Applicative t => (b -> t b) -> (a -> t a) The name Traversal will become clear later. The first thing we notice is that every lens is a traversal, becauase every Applicative is a Functor: lensToTraversal :: Lens a b -> Traversal a b lensToTraversal l = l I wrote this function only to show you that the types check, but we can just use a lens as a traversal directly! The other direction does not work, because not every Functor is an Applicative. ## Generalizing over So whatever a Traversal is, it is more general than a Lens. Thus, if we can change some of our functions to take a Traversal instead of a Functor, then the world is strictly a better place. Can we change the type of over as follows? over :: Traversal a b -> (b -> b) -> (a -> a) over l f a = unI$ l f' a
where f' b = MkI f b

Yes we can! Well, almost, the compiler wants us to provide an Applicative instance for I. Fine with me:

instance Applicative I where
pure x = MkI x
f <$> x = MkI$ (unI f) (unI x)

Since set is just defined in terms over over, we can now also relax the type signature of set to use Traversal.

## Non-Lens traversals

Can we do the same thing with view? No, we cannot! The constant functor is not applicative (there is no way of implementing pure :: a -> C b a).

So a Traversal a b describes how one can (possibly effectful) set or update values of type b in a (like Lens), but not get a value of type b (unlike Lens). If we try to think of concrete a where that is the case, what come to mind?

For example Maybe b! We certainly can apply a function to the contained thing, if it is there, and maybe even with effect:

this :: Traversal (Maybe a) a
this f Nothing  = pure Nothing
this f (Just x) = Just <$> f x Here, we cannot expect to have a view because not every Maybe a has an a. Another example would be lists: elems :: Traversal [a] a elems f [] = pure Nothing elems f (x:xs) = (:) <$> f x <*> elems f xs

Here we cannot expect to have a useful view because a [a] might not have an a, but also because it might have many.

## Getting all of them

So we cannot have view because the structure might have zero or more than one elements. Well, then at least we should be able to get a list of them?

listOf :: Traversal a b -> a -> [a]

Again, we can try to implement that using a suitable Functor. We compare the desired type with the type of a Traversal and find that we again need a constant functor, this time, though, storing a list of bs:

newtype CL b x = MkC [b]

unCL :: CL b x -> [b]
unCL (MkCL b) = b

instance Functor CL where
fmap f (MkCL b) = MkCL (map f b)

instance Applicative CL where
pure _ = MkCL []
MkCL bs1 <*> MkCL bs2 = MkCL (bs1 ++ bs2)

With this constant functor, we can define view in terms of overF:

listOf l a = unCL \$ overF l MkCL a

(In reality one would use Const [b] x here with the Applicative instance for Const with the Monoid constraint on the first argument of Const, but since we did not discuss Monoid in this class, we do it by hand here.)

## What is a Traversal now?

Similar to Lens is one position in a data structure (and precisely one, and one that is always there), Traversal describes many position in a data structure.

And since Lens and Traversal compose so nicely, you can describe pretty complex “pointer” well. For example with xml-lens, this Traversal extracts the title of all books with a specific category from an XML fragment.

root . el "books" ./ el "book" . attributeIs "category" "Textbooks" ./ el "title" . text

The story presented here is rather simple. If you look at the lens libray you see more abstractions (Prism, Iso, etc.). This library also comes with a large number of concrete lenses, traversals etc for many data structures, and has cool tricks so that _2 for example is a lens for the second element of a tuple, for any tupel size.
In that package, what we called Lens and Traversal is actually called Lens' and Traversal', and the version without quote allows over to change the type of the thing pointed at.
But note that even in the lens library, all these notions are just type synonyms, so you can define lenses as we did, without using a library, and you are still compatible with these libraries! Also see microlens for a library, compatible with lens, but smaller, less dependencies and better documented.