Lenses, also known as functional references, are a powerful way of looking at, constructing, and using functions on complex data types. They're also, unfortunately, a very new and complex subject making them challenging to learn. This tutorial intends to help lay out the basics of lensing.
I'm here assuming that you're familiar with moderate complexity Haskell. Truly, understanding the use of lenses isn't terribly difficult, but the phrasing, type errors, and use of Template Haskell can be confusing. I suggest below that if you do not recognize a snippet of Haskell entirely, try to read through it. Again, the lens concept isn't challenging, even if it looks that way at first
The first complexity of lensing is that there are a variety of libraries offering "functional references" or "lenses", some of which are compatible and some of which aren't. These include data-accessor
, fclabels
, lenses
, data-lens
, and lens
. The newest, largest, and most active library is lens
offering the Control.Lens
module and the remainder of this article uses it.
import Control.Lens
Feel free to take a look at the Haddock documentation, but beware it's quite dense, terse, and challenging. Lenses are a simple concept, but also very general.
What is a lens anyhow?
At its simplest, a lens is a value representing maps between a complex type and one of its constituents. This map works both ways—we can get or "access" the constituent and set or "mutate" it. For this reason, you can think of lenses as Haskell's "getters" and "setters", but we shall see that they are far more powerful.
data Arc = Arc { _degree :: Int, _minute :: Int, _second :: Int }
data Location = Location { _latitude :: Arc, _longitude :: Arc }
-- This is a TH splice, it just creates some functions for us automatically based on the record functions in 'Location'. We'll describe them in more detail below.
$(makeLenses ''Location)
Here we generate some lenses automatically for a record type Location
using Template Haskell. Lenses are easy to write on your own, but we'll treat them as black boxes for a while.
The underscores in the record names
_degree
,_minute
, etc. are aControl.Lens
convention for generating TH.
The result of this TH splice is the creation of two lenses, one corresponding to each field of the record.
latitude :: Lens' Location Arc
longitude :: Lens' Location Arc
If you're following along in GHC, though, you'll get a bit of a surprise already. The inferred types of these lenses are quite exotic, like
latitude :: Functor f => (Arc -> f Arc) -> Location -> f Location
, betraying thatLens'
is simply a strangetype
synonym. We'll understand this in much more depth later, but at first it's required to just remember the simpler typeLens'
.
We can use lenses as both getters and setters on Location
types.
getLatitude :: Location -> Arc
getLatitude = view latitude
setLatitude :: Arc -> Location -> Location
setLatitude = set latitude
Which is so simple it almost makes us wonder why we ever bothered with the whole lens concept! After all, we can already write getters and setters using record syntax.
getLatitudeR :: Location -> Arc
getLatitudeR (Location { _latitude = lat }) = lat
setLatitudeR :: Arc -> Location -> Location
setLatitudeR lat loc = loc { _latitude = lat }
so all we've bought so far with lenses is the ability to wrap these two functions up into a single value. More power is to come, but this intuition is a great first step. In fact, it's the second way we'll see to build lenses, using the function lens :: (c -> a) -> (c -> a -> c) -> Lens' c a
which takes a getter and setter and combines them into a lens!
Using lens
we can see how getters and setters turn into lenses and even note a law of lenses
lens getLatitudeR (flip setLatitudeR)
=== -- we can replace the getters and setters with their lens versions
lens getLatitude (flip setLatitude)
=== -- which have these definitions
lens (view latitude) (flip $ set latitude)
=== -- which is identical to
latitude
-- OR, for all lenses, l
l == lens (view l) (flip $ set l)
First joys of abstraction
So what exactly do we buy, wrapping "getters" and "setters" up together? Well, for one, we can forgo record syntax (for better or worse) and export just the lenses instead of the record functions if we like. For another, we can have other kinds of combinators to operate on these lenses for affecting the "focal" record values.
For instance, modification is immediately a combinator, over
(and this is built in to the library itself)
modifyLatitude :: (Arc -> Arc) -> (Location -> Location)
modifyLatitude f = latitude `over` f
-- which wraps the motif
modifyLatitude :: (Arc -> Arc) -> (Location -> Location)
modifyLatitude f lat = setLatitude (f $ getLatitude lat)
So, over
allows us lift a function between the getter and the setter, to create a function which modifies just a tiny part of the greater whole. Really, over
is nothing special—we've trivially built it from getLatitude
and setLatitude
, but you can begin to see the difference in thought. All of these various update/accessor functions have been rolled into a single value,
We can thus think of a lens as focusing in on a smaller part of a larger object.
That intution is powerful.
Building telescopes
So now that we have a basic understanding of lenses, let's build some more.
$(makeLenses ''Arc)
getDegreeOfLat :: Location -> Int
getDegreeOfLat = view degree . view latitude
setDegreeOfLat :: Int -> Location -> Location
setDegreeOfLat = over latitude . set degree
Perfect! We can compose our getter and setter functions to dive more deeply. We could even combine these deeper, "more focused" lenses to form a new lens.
degreeOfLat'Manually :: Lens' Location Int
degreeOfLat'Manually = lens getDegreeOfLat (flip setDegreeOfLat)
But this is getting a little out of control. Haskell is all about having mind-sized chunks of computational value which we combine meaningfully. Is there a way to directly combine lenses? Yes, and the method may come as a surprise
degreeOfLat = latitude . degree -- well, that was easy!
Lenses, with their weird underlying type involving forall
s and functions of Functor
s compose... much like functions do! The only difference is you read the composition right-to-left instead of the usual left-to-right function chaining. This can be a little confusing for a functional programmer, but if you squint it looks a lot like nested property referencing on an object in an OO language.
As an aside, while all of the lens libraries appreciate and allow for easy composition of lenses, only certain representations can be combined using
(.)
and it was quite a breakthrough to discover this. The details will be fleshed out later.
You might think of these as placing two (real world) lenses in series—together they refine the optics to focus more and more deeply into the subject.
(.) :: Lens' a b -> Lens' b c -> Lens' a c
Other kinds of optics
If lens composition gives us telescopes, can we build other kinds of optical machinery?
Let's look at other basic ways of composing Haskell types. Perhaps the two most essential methods are pairs and eithers, i.e. (,)
and Either
.
Lenses can be combined in ways analogous to the first two—you can link two lenses to operate in parallel using alongside
(forming a pair of glasses, perhaps)
latitude `alongside` longitude :: Lens' (Location, Location) (Arc, Arc)
and you can link two lenses such that either the first or the second is used
choosing degree minute :: Lens' (Either Arc Arc) Int
which you might think of as "teeing" two beams of lensed light together—like a beam splitter run in reverse. It lets us take lenses which focus from different locations but into the same type and combine then.
Postscript
If this were truly all there were to lenses it might be enough to find a place in your toolkit. They provide a new abstraction—the idea of holding on to a value that's focused on a constituent of a larger type—and a meaningful algebra for combining (via pairs and eithers, products and coproducts), composing, and modifying these values. They subsume record syntax while minimizing book-keeping on getters and setters. They even include a cute syntax throwing lenses over
functions.
But this is just one module of the more than 40 included in the library---indeed, nothing in this article besides the unnecessary makeLenses
Template Haskell tricks exists outside of Control.Lens.Lens
.
Furthermore, the same trick that allows us to compose lenses via (.)
unlocks a very general methodology for thinking about mapping and traversing over data structures that can be taken much further. Later we'll see how to use lenses within monads, to build powerful roundtrip transformations, abstract across cons'able or index'able data structures, or even to subsume zippers and generics.
We'll also see how the underlying Rank 2 structure of the lens allows for all of this functionality to be easily composed.
So, until next time—cheers!
Thanks to Serge Le Huitouze for edits.