Let's learn about lenses
Posted on August 21, 2018
Before we begin: in order to understand a lot of the following you’ll first need some familiarity with Haskell syntax and be somewhat comfortable with what a Functor is.
OK, now firstly: why learn about lenses? Every time I’ve seen lenses in action it’s looked like wizardry. I mean, just look at these examples of using
What? Reaching into some JSON, transforming it and just putting it back together, there’s no way it can be so easy? Right?
Let’s try to understand what’s going on here. We should probably start with something a little simpler than the JSON example above, how about tuples?
OK, imagine we have these two magic functions.
_1 for doing stuff to the first element of a tuple and
_2 for doing stuff to the second.
We can compose them
_1 . _ 2, which lets us do stuff to the second element of a tuple which is itself the first element of a tuple.
What kind of stuff?
We can “view” (
^.) the value:
We can “modify” (
%~) the value:
We can traverse over a list of these tuples and view the value of each as a list (
While we traverse (this time over any Traversable thing), instead of viewing we can modify:
That looks pretty useful to me. What do you reckon?
To understand how this all works we should probably move step by step. Let’s try this:
- First we’re going to define slightly simplified versions of
- Then we’ll move on to define similarly simplified versions of
- Finally we’ll adapt
_2to be a little closer to their definitions in the
We might seek to explore
^.. and how it interacts with
traverse in another post. It’s super cool, but we’ll likely need to get there via some other concepts.
_1 (one) and
What are these two magic functions? How do they work?
We’ll start by describing “functional references” al la Twan van Laarhoven, a precursor to what we now call lenses. For the sake of simplicity we’re still going to refer to these as “lenses”.
In the above article a lens is defined as something with this type:
Let’s make an alias for use later.
The goal of this section is going to be implementing functions that satisfy this type. In the next section we’ll seek to understand why this type is the way it is.
s stands for “structure”, this will become clearer a little later.
Something bothered me when I first saw this type, it looks like it’s missing two functions:
s -> a,
a -> s. If we had an
s -> a we could turn the
s we get as a second argument into the
a we need to supply to the
a -> f a we receive as the first argument. If we had an
a -> s we could use
fmap to transform the
f a to an
f s ready to return it.
We could write a function which accepts an
s -> a and an
a -> s in order to build a lens, something like this:
It ends up those functions are precisely what we need to provide to make a lens. The first function is the “getter”, the second the “setter”.
What might a getter for the first element of a tuple look like?
Looks about right. How about the setter?
That should work.
We don’t really need this
makeLens function, so let’s get rid of it.
We now do the getting by pattern matching and the setting with an anonymous function:
\a' -> (a', c). We don’t need separate functions necessarily for getting and setting, which might explain why they’re not mentioned in the type.
Let’s compare the type of
one to the lens type above.
s in the lens type is
(a, c) in the type for
one. The getter function
s -> a is generic enough to say that “
a is some part of
s”, without being specific about what
s is at all.
two is going to be pretty much the same, except that we’ll focus on the second element of the tuple.
If we compare it with the lens type:
All together now:
I reckon we’ve got a reasonable idea now how the functions
two are lenses. That is to say: we can see how the types line up.
The next step is to understand how we can use each of these functions as both a getter and a setter.
(^.) (view) and
We’ve made the types line up, but to what end? What is this crazy type up to anyway? The genius here lies largely in the
Functor f constraint.
To start with we’ll pick a very boring Functor to stand in for
See how boring this thing is? It’s just a container for a single value, all
fmap can do is unpack it to get at the
a inside, apply
a and pack it back up again.
Note that our lenses expect a
a -> f a, which in this case will be
a -> Identity a. We have a function already which can fulfil that type,
Identity’s only data constructor:
So we can pass the
Identity data constructor as the first argument to
one and see what we get.
Hmm, and if we pass it some value?
Makes sense, and I guess all we can do now is unpack it using
runIdentity :: Identity a -> a:
Wait, this looks familiar.
Right, right. Cool. So absolutely nothing happens but in a fairly involved way.
We can make something happen though.
We can compose
Identity :: a -> Identity a with some
a -> a. We can write a pretty general function for this:
… an even more general function:
Wow, what’s the type of
Modify takes a lens:
a -> a, which will be composed with
Identity :: a -> Identity a, still giving us the
a -> Identity a we need:
And some value the lens can act on:
If we make things less generic for a second it might be a bit clearer:
one :: (a -> Identity a) -> (a, c) -> Identity (a, c) one aToFa (a, c) = fmap (\a' -> (a', c)) (aToFa a) -- ^^^^^ ^^^^^^^^^^^^^^^^^^^^^ -- | All this does is put the -- | modified a back in the tuple: -- | Identity a -> Identity (a, c) -- | -- | -- This is the Identity data constructor composed -- with some function a -> a, the modification
This is all pretty interesting but still doesn’t answer why we have the
Functor f constraint,
Identity here just makes getting to the value kinda obscure.
Let’s put another weird Functor in place of
This was pretty mind-bending for me at first. What possible use could this thing have? It looks like it’s a Functor where
fmap doesn’t do anything, the function argument is totally ignored.
It’s worth having another look at the signature for
fmap in that last code block. The function argument isn’t applied, but the type does change. We’ve still gone from
f a to
f b after we’ve
Let’s look at these
fmaps side by side.
What happens when we use
Const :: a -> Const a a as our
a -> f a in
Huh? Weird. Wonder what the type is.
Right, so we do have a
Const a (a, c), which fits the
f (a, c) returned by
one. The really interesting thing is that the “setter” part of
one didn’t actually get applied.
fmap ignores that function argument.
Once more, if we make things less generic for a second it might be a bit clearer:
The Functor constraint is satisfied, so we’re able to use
Const interchangeably. When we use
Identity the setter function is run and so the updated value is popped back into our tuple, when we use
Const the setter is skipped.
Once again there’s little more we can do than unwrap the Functor:
Also once again, we can write a very generic version of this:
modify is somewhat limited, we can only provide it functions that accept and return values of the same type (
a -> a). What might it take to adapt
modify to work with functions that accept one type and return another?
The answer is: nothing. All we need to do is change some type signatures and variable names. The actual functions are already good to go.
Our current lens type becomes the same as the lens library type by adding two more type variables:
t. When we allow the value yanked out of
s to have it’s type changed from
b, putting it back must be allowed to change the type of
s, and so we return
We can define another handy alias.
_1 with only some type and value variable renaming:
view gets much the same treatment, we also flip the args to make it match
And hey presto we have polymorphic lenses, quite like those in
Some more fun examples to see what we’ve done in action:
This type didn’t make a heap of sense to me when I first saw it:
After having gone through all of the implementations above, I reckon I get it now. This is not to say I understand all the weird and wonderful things in lens, but I feel like I’m on the way. With any luck this may have helped you too.