Basic Lensing

As of March 2020, School of Haskell has been switched to read-only mode.

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 a Control.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 that Lens' is simply a strange type synonym. We'll understand this in much more depth later, but at first it's required to just remember the simpler type Lens'.

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 foralls and functions of Functors 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.

comments powered by Disqus