This post contains some notes from watching this great video by Simon Peyton Jones on what a lens in Haskell is. If you are interested, I recommend you watch the video, as the following are just my notes for future reference.
Very simply, a lens is something that allows you to access and modify parts of a data-structure.
Before continuing, we will need some imports and GHC extensions.Suppose you have the following record types and an example fred
.
With lenses, our goal is to be able to access and modify parts of these records, say, update the street name of where Fred lives.
We need two things: a way to view
part of a record, and a way to set
part of a record. Given a structure s
and a focus a
, we come up with the following:
The function viewR
on a LensR s a
takes a structure s
and returns the value of the focus. Let’s make a lens that allows us to view/set the name of the employee.
Let’s try using it by changing Fred’s name to “Bob”.
That looks like it worked! Let’s make some more lenses. One for the address field, and one for the street of the address.
Now, a nice property of lenses would be if they composed. For example, say you want to change the street Fred lives on. You will need a composeR
function of type LensR s1 s2 -> LensR s2 a -> LensR s1 a
. Here’s how it will look. It’s really nothing special, you just need a little bit of thought. A view of the composition is the composition of the views, and a set of a composition is first viewing the underlying record, updating it with its set function, and then setting the whole thing back into the larger structure.
Let’s confirm it works.
This is all well and good. But now suppose you want to do something more complicated, like update a field with a function. This operation would have the type (a -> a) -> s -> s
. What if you needed the function to do some possibly failing computation? The type would then be (a -> Maybe a) -> s -> Maybe s
. What if it needed IO? We would get a -> IO a) -> s -> IO s
.
This brings us to the following main idea of the person who invented lenses.
In fact, this function type is powerful enough to represent LensR
that we developed above. The two are isomorphic. If two types a
and b
are isomorphic, it means there exist functions f :: a -> b
and g :: b -> a
such that f . g = id
and g . f = id
. Try figuring out the isomorphism by yourself before continuing.
Let’s show that we can get from a Lens'
to a LensR'. We start with
set`.
How does this work? It works by picking the Functor f
in the above typealias to be the Identity
functor! The function set_fld
takes the type a -> Identity a
, ignores the current value, and sets it to whatever set
got. In the above, you see two versions that are equivalent, just the second one eta abstracts the s
.
What about viewing? In a similar fashion, we will use the Const v
functor.
The function between Lens' s a
and LensR s a
is then simply:
It is now also easy to create a function that we talked about before of type (a -> a) -> s -> s
.
Let’s do the opposite direction now. Try it yourself first. There really isn’t much you can do for the types to match up.
We can test it out.
Cool! That works. But how do you actually make a Lens'
?
Think of the f
as a function that takes a new value for a field, and applies it to a record with a hole there. You can easily construct these with template haskell. This is what happens in the Control.Lens library.
How about Lens'
composition? Look at the types. Conveniently, as Lens'
are just functions, their composition is just pure function composition.
The fantastic thing is that all this will work for any functor.
Let’s look at two applications that Simon noted in his talk. First, how about virtual/derived fields? The following record only stores temperature as Fahrenheit, but we can make a celcius lens that allows us to view and set the temperature as if it was in degrees celcius.
Another example is maintaining variants. In the following data structure, we store hours and minutes. What would happen though if we added 20 minutes to a record that already has 50 minutes? It would overflow. We can maintain the invariants with a lens that looks like this.
Check it out:
### Conclusion
I hope this was useful, even if just a very brief introduction to lenses in Haskell.