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
askUser = do
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 b
s:
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
Further reading
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.