Lenses You Can Make at Home
April 26, 2014
The most striking traits of the
lens library are its astonishing breadth and generality. And yet, the whole edifice is built around van Laarhoven lenses, which are a simple and elegant concept. In this hands-on exposition, I will show how the
Lens type can be understood without prerequisites other than a passing acquaintance with Haskell functors. Encouraging sound intuition in an accessible manner can go a long way towards making
lens and lenses less intimidating.
I will define a toy data type so that we have something concrete to play with, as well as a starting point for working out generalisations.
The record definition gets us a function for accessing the
As for the setter, we have to define it ourselves, unless we feel like mucking around with record update syntax.
Armed with a proper getter and setter pair, we can easily flip the sign of the
bar inside a
We can make it even easier by defining a modifier function for
setBar can be recovered from
modifyBar by using
const to discard the original value and put the new one in its place.
If our data type had several fields, defining a modifier for each of them would amount to quite a lot of boilerplate. We could minimise it by, starting from our
modifyBar definition, abstracting from the specific getter and setter for
bar. Here, things begin to pick up steam. I will define a general
modify function, which, given an appropriate getter-setter pair, can deal with any field of any data type.
It is trivial to recover
modifyBar; when we do so,
The next step of generalisation is the one leap of faith I will ask of you in the way towards lenses. I will introduce a variant of
modify in which the modifying function, rather than being a plain
a -> a function, returns a functorial value. Defining it only takes an extra
And here is its specialisation for
Why on Earth we would want to do that? For one, it allows for some nifty tricks depending on the functor we choose. Let’s try it with lists. Specialising the
modifyF type would give:
Providing the getter and the setter would result in a
(a -> [a]) -> s -> [s] function. Can you guess what it would do?
As the types suggest, we get a function which modifies the field in multiple ways and collects the results.
I claimed that moving from
modifyF was a generalisation. Indeed, we can recover
modify by bringing
Identity, the dummy functor, into play.
We wrap the field value with
Identity value after applying
k and unwrap the final result after applying the setter. Since
Identity does nothing interesting to the wrapped values, the overall result boils down to our original
modify. If you have found this definition confusing, I suggest that you, as an exercise, rewrite it in pointful style and substitute the definition of
We managed to get
modify back with little trouble, which is rather interesting. However, what is truly surprising is that we can reconstruct not only the modifier but also the getter! To pull that off, we will use
Const, which is a very quaint functor.
If functors were really containers,
Const would be an Acme product. A
Const a b value does not contain anything of type
b; what it does contain is an
a value that we cannot even modify, given that
fmap f is
id regardless of what
f is. As a consequence, if, given a field of type
a, we pick
Const a as the functor to use with
modifyF and use the modifying function to wrap the field value with
Const, then the value will not be affected by the setter, and we will be able to recover it later. That suffices for recovering the getter.
The Grand Unification
Given a getter and a setter,
modifyF gets us a corresponding functorial modifier. From it, by choosing the appropriate functors, we can recover the getter and a plain modifier; the latter, in turn, allows us to recover the setter. We can highlight the correspondence by redefining once more the recovered getters and modifiers, this time in terms of the functorial modifier.
modify'' :: ((a -> Identity a) -> s -> Identity s) -> (a -> a) -> s -> s modify'' modifier k = runIdentity . modifier (Identity . k) modifyBar'' :: (Int -> Int) -> Foo -> Foo modifyBar'' = modify'' modifyBarF set :: ((a -> Identity a) -> s -> Identity s) -> s -> a -> s set modifier x y = modify'' modifier (const y) x setBar'' :: Foo -> Int -> Foo setBar'' = set modifyBarF get' :: ((a -> Const a a) -> s -> Const a s) -> (s -> a) get' modifier = getConst . modifier Const getBar' :: Foo -> Int getBar' = get' modifyBarF
The bottom line is that given
modifyBarF we can get by without
get' allow us to reconstruct them whenever necessary. While our first version of
get was, in effect, just a specialised
const with a wacky implementation,
get' is genuinely useful because it cuts the number of separate field manipulation functions we have to deal with by a third.
Even after all of the work so far we can still generalise further! Let’s have a second look at
The type of
(s -> a -> s); however, nothing in the implementation forces the first argument and the result to have the same type. Furthermore, with a different signature
k could have a more general type,
(a -> f b), as long as the type of
setter was adjusted accordingly. We can thus give
modifyF a more general type.
For the sake of completeness, here are the generalised recovery functions.
get is not included because the generalisation does not affect it.
By now, it is clear that our getters and setters need not be ways to manipulate fields in a record. In a broader sense, a getter is anything that produces a value from another; in other words, any function can be a getter. By the same token, any binary function can be a setter, as all that is required is that it combines one value with another producing a third; the initial and final values do not even need to have the same type.1 That is a long way from the toy data type we started with!
If we look at
modifyGenF as a function of two arguments, its result type becomes:
Now, let’s take a peek at Control.Lens.Lens:
It is the same type! We have reached our destination.2 A lens is what we might have called a generalised functorial modifier; furthermore, sans implementation details we have that:
lensspecialised to produce simple lenses;3
modifyBarFis a lens with type
Lens Foo Foo Int Int;
lens uses type synonyms liberally, so those correspondences are not immediately obvious form the signatures in the documentation. Digging a little deeper, however, shows that in
ASetter is merely
Analogously, we have
Behind the plethora of type synonyms -
Iso and so forth - there are different choices of functors,5 which make it possible to capture many different concepts as variations on lenses. The variations may be more general or less general than lenses; occasionally they are neither, as the overlap is just partial. The fact that we can express so much through parametrization of functors is key to the extraordinary breadth of
This exposition is primarily concerned with building lenses, and so very little was said about how to use them. In any case, we have seen enough to understand why lenses are also known as functional references. By unifying getters and setters, lenses provide a completely general vocabulary to point at parts of a whole.
Finally, a few words about composition of lenses are unavoidable. One of the great things about lenses is that they are just functions; even better, they are functions with signatures tidy enough for them to compose cleanly with
(.). That makes it possible to compose lenses independently of whether you intend to get, set or modify their targets. Here is a quick demonstration using the tuple lenses from
A perennial topic in discussions about
lens is the order of composition of lenses. They are often said to compose backwards; that is, backwards with respect to composition of record accessors and similar getters. For instance, the getter corresponding to the
_1 . _2 lens is
snd . fst. The claim that lenses compose backwards, or in the “wrong order”, however, are only defensible when talking about style, and not about semantics. That becomes clear after placing the signatures of a getter and its corresponding lens side by side.
The getter takes a value of the source type and produces a value of the target type. The lens, however, takes a function from the target type and produces a function from the source type. Therefore, it is no surprise that the order of composition differs, and the order for lenses is entirely natural. That ties in closely to what we have seen while implementing lenses. While we can squeeze lenses until they give back getters, it is much easier to think of them as generalised modifiers.
We are not quite as free when it comes to pairing getters and setters. Beyond the obvious need for getter and setter to start from values of the same type, they should behave sanely when composed. In particular, the following should hold:↩
“What about the
forall?” you might ask. Are we cheating? Not quite. The
forallis there to control how
fis specialised when lens combinators are used. The underlying issue does not affect our reasoning here. If you are into type system subtleties, there were a few interesting comments about it in the reddit thread for this post.↩
Lens' s aor
Lens s s a a, as opposed to
Lens s t a b.↩
And in some cases of profunctors to replace the function type constructor.↩
Post licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.