A lens is a getter and a setter for one field, packaged as a single value that composes with ordinary function composition. This builds the naive record-of-two- functions version, hits the wall that version has, replaces it with the actual van Laarhoven representation the library uses, and then works through the rest of the optics hierarchy - prisms, traversals, isos, folds - on top of that same representation.
The naive representation, and its wall
A lens as a pair of functions: get the field, and set it, returning a new whole value.
data Lens s a = Lens{ view :: s -> a, set :: a -> s -> s}
Two records, and a lens onto one field of each.
data Point = Point { _px :: Double, _py :: Double } deriving Showdata Circle = Circle { _center :: Point, _radius :: Double } deriving Showpx :: Lens Point Doublepx = Lens _px (\a s -> s { _px = a })center :: Lens Circle Pointcenter = Lens _center (\a s -> s { _center = a })
Composing two of these by hand - the getter composes forwards, the setter composes in a criss-cross that gets uglier with every extra level.
compose :: Lens s a -> Lens a b -> Lens s bcompose outer inner = Lens{ view = view inner . view outer, set = \b s -> set outer (set inner b (view outer s)) s}centerX :: Lens Circle DoublecenterX = compose center px
Using it - reads fine so far.
ghci> let c = Circle (Point 1 2) 5ghci> view centerX c1.0ghci> set centerX 99 cCircle {_center = Point {_px = 99.0, _py = 2.0}, _radius = 5.0}
The wall: `modify` (update in place, given the old value) has to call view and set separately, which means every update walks the structure twice.
modify :: Lens s a -> (a -> a) -> s -> smodify l f s = set l (f (view l s)) s
Worse: nothing about this Lens type composes with a Maybe, an Either, a list, or any other functor-shaped access. A lens onto a record field and a prism onto a constructor end up as two unrelated types with no shared vocabulary, and every combinator has to be reinvented per shape.
(* Lens s a composes with Lens a b via the hand-written `compose` above. *)(* A Prism onto an Either branch, or a Traversal over a list, would need *)(* its own separate composition function - none of it unifies. *)
The van Laarhoven representation
The fix is to stop representing a lens as data, and represent it as a function polymorphic over any Functor. This one change is what lets every optic - lens, prism, traversal, fold - become the same underlying shape, just instantiated at a different functor.
The type. It looks unmotivated until you use it - a lens is a natural transformation lifting a function s -> f s from acting on the field to acting on the whole.
{-# LANGUAGE RankNTypes #-}type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s
Building one from the same getter/setter pair as before - lens is the smart constructor that turns familiar get/set into this shape.
lens :: (s -> a) -> (s -> a -> s) -> Lens' s alens getter setter afa s = fmap (setter s) (afa (getter s))px :: Lens' Point Doublepx = lens _px (\s a -> s { _px = a })center :: Lens' Circle Pointcenter = lens _center (\s a -> s { _center = a })
Composition is now just (.) - ordinary function composition, because a Lens' is just a function. This is the entire payoff of the representation change.
centerX :: Lens' Circle DoublecenterX = center . px
view instantiates f at Const a, whose Functor instance ignores the update entirely - running the lens at this functor extracts only the getter behavior.
newtype Const a b = Const { getConst :: a }instance Functor (Const a) wherefmap _ (Const a) = Const aview :: Lens' s a -> s -> aview l s = getConst (l Const s)
set and over instantiate f at Identity, whose Functor instance just applies the function - running the lens at this functor performs the update.
newtype Identity a = Identity { runIdentity :: a }instance Functor Identity wherefmap f (Identity a) = Identity (f a)over :: Lens' s a -> (a -> a) -> s -> sover l f s = runIdentity (l (Identity . f) s)set :: Lens' s a -> a -> s -> sset l a = over l (const a)
One walk of the structure does both the read and the write now - view and set were separate walks before; over is a single pass because the functor carries both the extracted value and the rebuilt structure through the same traversal.
ghci> view centerX (Circle (Point 1 2) 5)1.0ghci> over centerX (+ 100) (Circle (Point 1 2) 5)Circle {_center = Point {_px = 101.0, _py = 2.0}, _radius = 5.0}
This is, field for field, lens\'s own Lens type. Everything above is what the library gives you for free.
type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s
Using the real library
Install it.
$ cabal install lens
makeLenses generates a Lens\' for every underscore-prefixed field automatically, via Template Haskell - the by-hand lens function above is what this expands to.
{-# LANGUAGE TemplateHaskell #-}import Control.Lensdata Point = Point { _px :: Double, _py :: Double } deriving ShowmakeLenses ''Pointdata Circle = Circle { _center :: Point, _radius :: Double } deriving ShowmakeLenses ''Circle
What it generates - the field names, with the leading underscore stripped.
-- generates:-- px :: Lens' Point Double-- py :: Lens' Point Double-- center :: Lens' Circle Point-- radius :: Lens' Circle Double
The operators, which is what code actually looks like day to day. ^. is view, .~ is set, %~ is over, all infix.
let c = Circle (Point 1 2) 5c ^. center . px-- 1.0c & center . px .~ 99-- Circle {_center = Point {_px = 99.0, _py = 2.0}, _radius = 5.0}c & center . px %~ (+ 100)-- Circle {_center = Point {_px = 101.0, _py = 2.0}, _radius = 5.0}
& is just reverse function application - reads left to right as 'take c, then apply this lens update', which is the whole reason it shows up so often in lens code.
(&) :: a -> (a -> b) -> bx & f = f x
Chaining several updates in one expression - each %~/.~ is independent, applied left to right.
c & center . px %~ (+ 1)& center . py %~ (* 2)& radius .~ 10
Numeric operators specialized for the common case, so you rarely write %~ (+ n) by hand.
c & center . px +~ 1 -- addc & center . px -~ 1 -- subtractc & center . px *~ 2 -- multiplyc & radius //~ 2 -- dividec & center . px <>~ 0 -- mappend, for Monoid fields
Traversals: more than one target
A Lens' always finds exactly one target. A Traversal' generalizes that to zero-or-more, by requiring Applicative instead of just Functor - the extra power buys the ability to sequence effects across multiple targets rather than just one.
The type - one character different from Lens\', and that difference is exactly what allows more or fewer than one focus.
type Traversal' s a = forall f. Applicative f => (a -> f a) -> s -> f s
traverse itself is a Traversal' over every element of any Traversable - Functor is not strong enough to sequence effects across a list of unknown length, which is exactly why Traversal needs Applicative.
let xs = [1, 2, 3, 4, 5] :: [Int]xs ^.. traverse-- [1,2,3,4,5] (toListOf, collects every focus)xs & traverse %~ (* 10)-- [10,20,30,40,50]xs ^? traverse-- Just 1 (preview, the first focus only)
Composing a Lens\' with a Traversal\' focuses every element reachable through the lens - each of a list of circles, updated through the same center . px path used above on one.
let circles = [Circle (Point 1 2) 5, Circle (Point 3 4) 6]circles ^.. traverse . center . px-- [1.0, 3.0]circles & traverse . center . px %~ (+ 100)-- [Circle {_center = Point {_px = 101.0, ...}, ...}, ...]
filtered narrows a traversal to targets matching a predicate - only radii above 5 get doubled, everything else passes through untouched.
circles & traverse . radius . filtered (> 5) %~ (* 2)
each traverses every field of a tuple uniformly, when they share a type.
(1, 2, 3) & each %~ (* 10)-- (10, 20, 30)
A worked Traversal\' by hand, for a type outside Traversable - traverse gets replaced with your own function shaped the same way, and it composes with lens/traversal machinery exactly like the library\'s own.
data Pair a = Pair a a deriving Showboth' :: Traversal' (Pair a) aboth' f (Pair x y) = Pair <$> f x <*> f yPair 1 2 & both' %~ (* 10)-- Pair 10 20
Prisms: optional, constructor-shaped access
A lens assumes the target always exists. A Prism' is for a sum type\'s constructor, which may or may not match - it can build a value going one direction, and only sometimes deconstruct one going the other.
prism\' takes the constructor and a partial match function returning Maybe - review runs it forwards, preview runs it backwards and fails as Nothing when the shape does not match.
data Shape = SCircle Double | SRect Double Double deriving Show_SCircle :: Prism' Shape Double_SCircle = prism' SCircle (\s -> case s ofSCircle r -> Just r_ -> Nothing)_SRect :: Prism' Shape (Double, Double)_SRect = prism' (uncurry SRect) (\s -> case s ofSRect w h -> Just (w, h)_ -> Nothing)
review builds; preview attempts to extract and yields Maybe.
review _SCircle 5-- SCircle 5.0preview _SCircle (SCircle 5)-- Just 5.0preview _SCircle (SRect 3 4)-- Nothing
^? is preview and # is review, as infix operators - matching the ^. / .~ family used for lenses above.
SCircle 5 ^? _SCircle-- Just 5.0_SCircle # 5-- SCircle 5.0
over on a prism only touches the value when the shape matches - it is a no-op, not an error, on a mismatch, which is the entire point of it being optional rather than partial.
SCircle 5 & _SCircle %~ (* 2)-- SCircle 10.0SRect 3 4 & _SCircle %~ (* 2)-- SRect 3.0 4.0 (untouched: the shape did not match)
makePrisms generates one prism per constructor automatically, the same way makeLenses generates one lens per field - _SCircle and _SRect above are exactly what this produces.
data Shape = SCircle Double | SRect Double DoublemakePrisms ''Shape
_Just, _Nothing, _Left, _Right are prisms the library ships for the standard sum types, so Maybe and Either slot into the same optics vocabulary with no extra code.
Just 5 ^? _Just-- Just 5Left "err" ^? _Left-- Just "err"Just 5 & _Just %~ (+ 1)-- Just 6
Composing a lens with a prism reaches into a field that is itself a sum type, updating only when that field\'s shape matches - a Maybe Point field, focused past both the Maybe and the record in one path.
data Config = Config { _origin :: Maybe Point } deriving ShowmakeLenses ''ConfigConfig (Just (Point 1 2)) & origin . _Just . px %~ (+ 10)-- Config {_origin = Just (Point {_px = 11.0, _py = 2.0})}Config Nothing & origin . _Just . px %~ (+ 10)-- Config {_origin = Nothing} (no-op: nothing to focus on)
Isos: lossless, reversible conversions
An Iso\' witnesses that two types are interconvertible with no loss - unlike a lens, which discards nothing but also does not promise reversibility, an iso can be run backwards as freely as forwards.
newtype Celsius = Celsius Double deriving Shownewtype Fahrenheit = Fahrenheit Double deriving ShowcelsiusToFahrenheit :: Iso' Celsius FahrenheitcelsiusToFahrenheit = iso(\(Celsius c) -> Fahrenheit (c * 9/5 + 32))(\(Fahrenheit f) -> Celsius ((f - 32) * 5/9))
view runs it forwards; the from combinator flips it and view runs the flipped version backwards - one iso definition serves both directions.
Celsius 100 ^. celsiusToFahrenheit-- Fahrenheit 212.0Fahrenheit 32 ^. from celsiusToFahrenheit-- Celsius 0.0
_Wrapped/_Unwrapped, generated by makeWrapped for a newtype, is the library\'s built-in version of this exact pattern - the unwrap/wrap isomorphism a newtype always has for free.
newtype Age = Age Int deriving (Show)makeWrapped ''AgeAge 30 ^. _Wrapped-- 30(30 :: Int) ^. _Unwrapped'-- Age 30
Folds: read-only, and where they matter
A Fold\' is a Traversal\' restricted to Functor + Contravariant f rather than full Applicative, which is the type-level way of saying it can only be run through view/preview/toListOf, never through set or over.
type Fold' s a = forall f. (Contravariant f, Applicative f) => (a -> f a) -> s -> f s
folded turns an ordinary Foldable into a Fold\' - the read-only counterpart of traverse used above.
circles ^.. folded . radius-- [5.0, 6.0]sumOf (folded . radius) circles-- 11.0maximumOf (folded . radius) circles-- Just 6.0
The aggregating combinators built on Fold, which is most of why a Fold exists at all rather than just always using toListOf and a plain list function.
sumOf, productOf numeric aggregation over the targetslengthOf how many targets a fold hasanyOf, allOf, noneOf boolean aggregation with a predicateminimumOf, maximumOf Maybe-wrapped, since the fold may be emptytoListOf (= (^..)) every target, as a plain listfirstOf, lastOf Maybe-wrapped edges
to lifts an ordinary function into a Fold\', so you can splice a plain computation into an optics chain without leaving it.
circles ^.. folded . to (\c -> _radius c * 2)-- [10.0, 12.0]
Indexed optics
itraverse and ifolded carry the position alongside the value - the list index, the map key - without a separate zip against [0..] or Map.keys.
let xs = ["a", "b", "c"]xs ^@.. itraversed-- [(0,"a"), (1,"b"), (2,"c")]xs & itraversed %@~ (\i s -> show i <> ":" <> s)-- ["0:a", "1:b", "2:c"]
At for Map, both reading and inserting/deleting through the same optic - at k returns a Lens\' onto a Maybe, so setting it to Nothing deletes the key.
import qualified Data.Map as Mapimport Control.Lens.Atlet m = Map.fromList [("x", 1), ("y", 2)] :: Map.Map String Intm ^. at "x"-- Just 1m & at "z" ?~ 3-- fromList [("x",1),("y",2),("z",3)]m & at "x" .~ Nothing-- fromList [("y",2)]
ix, by contrast, is a Traversal\' - a no-op on a missing key rather than an insertion point, since it cannot create the position at k the way at can.
m & ix "x" %~ (+ 100)-- fromList [("x",101),("y",2)]m & ix "missing" %~ (+ 100)-- fromList [("x",1),("y",2)] (no-op)
Generic derivation without Template Haskell
generic-lens derives the same lenses via GHC.Generics rather than Template Haskell - no makeLenses call, no compile-time code generation step to wait on, at the cost of losing the underscore-stripped naming convention.
$ cabal install generic-lens
field\' looks a field up by its type-level Symbol name - the record needs no special preparation beyond deriving Generic.
{-# LANGUAGE DeriveGeneric, DataKinds #-}import Data.Generics.Product (field')import GHC.Generics (Generic)data Point = Point { px :: Double, py :: Double }deriving (Show, Generic)Point 1 2 ^. field' @"px"-- 1.0Point 1 2 & field' @"px" %~ (+ 10)-- Point {px = 11.0, py = 2.0}
A realistic composite: JSON, indexed, prism, traversal
lens-aeson gives Value its own optics vocabulary, so navigating parsed JSON reads exactly like navigating a record - the same %~/.~/^. family, over a dynamically typed tree instead of a static one.
$ cabal install lens-aeson
Reaching into nested, partially-unknown JSON - key for an object field, _String/_Integer as prisms onto a JSON scalar\'s shape, nth for an array index.
import Data.Aeson.Lensimport Data.Aeson (Value, decode)let Just v = decode"{\"users\":[{\"name\":\"ada\",\"age\":30},{\"name\":\"grace\",\"age\":45}]}" :: Maybe Valuev ^.. key "users" . values . key "name" . _String-- ["ada", "grace"]v ^? key "users" . nth 0 . key "age" . _Integer-- Just 30v & key "users" . values . key "age" . _Integer %~ (+ 1)-- every user's age incremented by one, structure otherwise untouched
Composed with itraversed to know which user is being touched while touching them, tying together indexed optics, prisms, and traversals in one expression.
v ^@.. key "users" . values . itraversed . key "name" . _String-- [(0,"ada"), (1,"grace")]
Most of the library reduces to composing four things: a Lens for one required field, a Prism for one branch that may not match, a Traversal for zero or more, and an Iso when the conversion is lossless both ways. Generic derivation gets you most of this for free, the same way Deriving Strategies in Haskell gets you the rest of a typeclass instance.