Optics from a Different Perspective

Why lenses don’t compose backwards

“How do I get to Aberdeen from here?”

“If I were going to Aberdeen, I wouldn’t start from here.”

I’m not sure that “lenses compose backwards” is still a live view. That said, I think this view is tied to some broader misunderstandings that are easy to fall into. The fact is that while lenses/optics are a really useful abstraction the historical (and often pedagogical) route leading up to them is really odd. We usually start by talking about getters and setters, but those are actually a somewhat awkward aspect of modern optics. My goal here is to present a different route to lenses that I think more directly captures the intuition behind them.

Semantic Editor Combinators

The starting point for this approach is an old Conal Elliott blog post about what he calls Semantic Editor Combinators. The basic idea is that when we think about functions like:

map   :: (a -> b) -> [a]   -> [b]
first :: (a -> b) -> (a,c) -> (b,c)
head  :: (a -> a) -> [a]   -> [a]
age   :: (Int -> Int) -> Person -> Person

we shouldn’t think of them as two argument functions that happen to be curried as a quirk of Haskell, but as genuinely single argument functions that transform an “edit” from operating on a “smaller” structure to operating on a “larger” structure. Furthermore, we should be perfectly happy to compose these: map . first . map . first takes an edit on a small structure and transforms it into an edit on a (much) larger structure.

It’s worth noting that SECs can be monomorphic (like age), polymorphic but type preserving (like head), or polymorphic and type changing (like map and first).

It’s also worth noting that—although it doesn’t show up in the types—some SECs call their argument function multiple times (like map), and some only once (like first).

Adding Effects

One shortcoming of SECs is that in a pure language like Haskell a decent chunk of the functions you’re going to want to call don’t have simple types, but types with some kind of “effect-carrying” wrapper: a -> f b. If we want to use something like an SEC on these we’re going to need to do something about the effect. If we focus for the moment on single target SECs it should be reasonably intuitive that we need this wrapper type to be a functor, so that after running the argument function we can do whatever we need to put the result back in our larger type. This gives types like:

first :: Functor f => (a -> f b) -> (a,c) -> f (b,c)
head  :: Functor f => (a -> f a) -> [a]   -> f [a]
age   :: Functor f => (Int -> f Int) -> Person -> f Person

which you may recognise as the “van Laarhoven” representation of lenses.

Operators

Operators in optics libraries have a bit of a messy reputation, and I’m not going to evangelise for them here. The way that operators (and similar functions) tend to work is:

You can make perfectly good use of optics without a lot of these operators by simply applying the optics as functions to your own small edit functions, although there are some pain points that we will cover (and to some extent solve) later.

Getters and Setters

This is where we rejoin the traditional route to thinking about optics. Setters are pretty obvious, they are simply applying the SEC to an edit function const x. Getters are much stranger in this approach. We start from the idea of applying

Changelog