Optics: Seeing Into Structure
Nested immutable records are painful to update by hand — Haskell's answer, lenses, turns out to be built from nothing but the Functor you already know, in a derivation short enough to fit on one page.
The problem: updating one field, three levels deep
Immutability (Chapter 5) means every “update” is really building a new value that shares as much structure as possible with the old one. For a flat record, that’s easy:
data Person = Person { name :: String, age :: Int }
birthday :: Person -> Person
birthday p = p { age = age p + 1 }
Nest a few more records inside each other, and the record-update syntax stops scaling:
data Address = Address { street :: String, city :: String }
data Company = Company { companyName :: String, hq :: Address }
data Employee = Employee { employeeName :: String, employer :: Company }
-- "just change the street" turns into rebuilding every layer by hand:
correctStreet :: String -> Employee -> Employee
correctStreet newStreet e = e
{ employer = (employer e)
{ hq = (hq (employer e)) { street = newStreet } }
}
Every layer between the value you actually want to change and the top of the structure has to be manually unpacked and rebuilt. This is the exact problem optics — and specifically lenses — exist to solve.
Deriving a lens from nothing but Functor
Here’s the genuinely startling part: a lens isn’t a new concept bolted onto the language. It’s built entirely out of Functor, in a definition short enough to fit in one line:
type Lens s a = forall f. Functor f => (a -> f a) -> s -> f s
Read it slowly: given any functor f, and a way to turn an a into an f a, a Lens s a turns a way to turn the whole structure s into an f s. That’s it — no new typeclass, no special machinery, just Functor (Chapter 9) applied at exactly the right type.
Figure: The same xLens function, run through two different functors — Const a gives a getter, Identity gives a setter — with no separate code written for either one.
A lens for one field of Point looks like this:
data Point = Point { _x :: Double, _y :: Double }
xLens :: Lens Point Double
xLens f (Point x y) = fmap (\x' -> Point x' y) (f x)
xLens takes a function f that can turn a Double into f Double for some functor f, applies it to _x, and uses fmap to rebuild a whole Point inside that same functor. Nothing here mentions “getting” or “setting” — that distinction only appears once you decide which functor f is.
Getting a getter and a setter for free
Two specific functors turn this one definition into the two operations you actually want:
newtype Const a b = Const { getConst :: a }
instance Functor (Const a) where
fmap _ (Const a) = Const a -- ignores the function entirely
view :: Lens s a -> s -> a
view l s = getConst (l Const s)
ghci> view xLens (Point 3 4)
3.0
Const is a functor that carries a value of type a around but refuses to let fmap touch it. Plugging Const in as f means xLens’s fmap becomes a no-op — the field just passes straight through, and getConst unwraps it at the end. That’s a getter, built with zero code written specifically for “getting.”
newtype Identity a = Identity { runIdentity :: a }
instance Functor Identity where
fmap f (Identity a) = Identity (f a)
set :: Lens s a -> a -> s -> s
set l newVal s = runIdentity (l (const (Identity newVal)) s)
ghci> set xLens 10 (Point 3 4)
Point {_x = 10.0, _y = 4.0}
Identity is the functor that does nothing but apply the function. Feeding it a constant function that always returns newVal means xLens rebuilds the whole structure with that new value spliced in — a setter, again built from the exact same xLens, just handed a different functor.
view and set are two different natural transformations applied to the same underlying structure-preserving map — which is exactly why choosing f is the only thing that changes between them. This is the same trick Chapter 9’s Functor laws guarantee will behave consistently: because fmap is required to respect structure regardless of which functor it’s specialized to, xLens is guaranteed to “do the right thing” for any functor you plug in, not just the two shown here.
Composition is just (.)
The single most delightful fact about lenses built this way: composing them needs no special operator at all. Ordinary function composition just works:
hqLens :: Lens Company Address
hqLens f c = fmap (\hq' -> c { hq = hq' }) (f (hq c))
employerLens :: Lens Employee Company
employerLens f e = fmap (\emp' -> e { employer = emp' }) (f (employer e))
streetLens :: Lens Address String
streetLens f a = fmap (\s' -> a { street = s' }) (f (street a))
-- zoom three levels deep, with plain old (.)
employeeStreet :: Lens Employee String
employeeStreet = employerLens . hqLens . streetLens
correctStreet :: String -> Employee -> Employee
correctStreet = set employeeStreet
employerLens . hqLens . streetLens reads left to right as “zoom into the employer, then the HQ, then the street” — the exact opposite reading order of ordinary function composition, and the reason lens composition feels like a small magic trick the first time you see it work.
This chapter builds one flavor of optic — a lens, for a field that’s always present. Sum types need a different optic, a prism, for a case that might not match at all (think Maybe’s Just, or one constructor of a larger data type) — the “set” direction still makes sense, but “get” has to be allowed to fail. Prisms, traversals (lenses that target many fields at once, like every element of a list), and how they all compose together into one unified hierarchy are exactly what the real optics libraries below are built around; this chapter’s hand-rolled Lens is deliberately the simplest member of that family.
Using a real library
Hand-deriving lenses works for teaching, but nobody hand-writes xLens-style boilerplate for every field of every record in a real codebase. Three libraries cover the space, in order of how much machinery they bring:
lens— the original, enormous, and famously comprehensive library, generating lenses automatically from record fields via Template Haskell (makeLenses ''Point), with prisms, traversals, and dozens of composable operators (^.,.~,%~) built on exactly theFunctor-based encoding above.optics— a newer, from-scratch redesign using a different internal encoding (profunctors rather than van Laarhoven functors), aimed at clearer type errors and a gentler learning curve, while covering the same conceptual ground.microlens— a deliberately minimal, dependency-light subset oflens’s API with no Template Haskell required, popular specifically for applications that want lens-style ergonomics without pulling in the full library.
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Point = Point { _x :: Double, _y :: Double } deriving Show
makeLenses ''Point -- generates x, y :: Lens' Point Double automatically
ghci> Point 3 4 ^. x -- view, via the (^.) operator
3.0
ghci> Point 3 4 & x .~ 10 -- set, via (&) and (.~)
Point {_x = 10.0, _y = 4.0}
ghci> Point 3 4 & x %~ (*2) -- over/modify, via (%~)
Point {_x = 6.0, _y = 4.0}
^., .~, and %~ are just view, set, and over (the fmap-through-a-lens operation) written as infix operators — the exact three functions this chapter derived by hand, generated automatically and composed with plain ., at real-codebase scale.
Lenses are genuinely one of Haskell’s most-exported ideas: Scala’s Monocle, PureScript’s profunctor-lenses, and even libraries in Python and TypeScript borrow the same get/set/compose shape, almost always crediting Haskell’s lens library as the reference implementation. Inside large Haskell codebases, deeply nested configuration records and JSON-shaped API responses are the two places lenses show up constantly — anywhere “change one field, three layers down” is a routine operation rather than a rare one.
Optics are also the first of this book’s “further peaks” — genuinely useful in production Haskell, built from ideas this book already gave you a name for, and a good sign of what the rest of the ecosystem tends to look like: a small, principled core idea, generalized until it covers an entire category of everyday problems.