lens: composable optics for Haskell records and what the four type parameters mean
Lenses, Folds, and Traversals - Join us on web.libera.chat #haskell-lens
At a glance
- What is it?
- lens is a Haskell library on Hackage providing lenses, folds, traversals, isomorphisms, getters and setters as composable optics. Its Template Haskell macros derive typed optics from record field declarations at compile time.
- Who is it for?
- For Haskell programmers working with nested records, `makeLenses ''Foo` produces a typed optic for each field. .~ is an infix alias for set, (.) from the Prelude composes any 2 optics, and `Lens'` can be used directly as a `Lens` when the type does not change. Before adopting the library, run the README's first ghci session against a record type from your own codebase to check which generated optic names appear.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly Haskell, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Install from Hackage; the README opens with a ghci session as the first live use
lens is published on Hackage. A README badge links to hackage.haskell.org/package/lens, the installation source. Add lens to the Cabal file and open a REPL.
ghci> import Control.LensReading the second element of a tuple:
ghci> ("hello","world")^._2
"world".~ is an infix alias for `set`. Writing to the first position:
ghci> _1 .~ "hello" $ ((),"world")
("hello","world")Writes in this library can change the type of the container. In `set _2 42 ("hello","world")`, the second element in the result shifts from a string to an integer:
ghci> set _2 42 ("hello","world")
("hello",42)Both before and after types sit in the optic itself. `view` is a prefix alias for `^.`. `(%~)` is an infix alias for `over`. Applying `over (mapped._2) succ [(1,2),(3,4)]` returns `[(1,3),(3,5)]`, applying `succ` to the second element of each pair. Reading and writing to tuple positions are the first operations shown, using nested tuples as the primary running examples throughout the opening session.
Lens carries 4 type parameters because a write can change the type of the whole
Applying `makeLenses` to a record:
data Foo a = Foo { _bar :: Int, _baz :: Int, _quux :: a }
makeLenses ''Foogenerates:
bar, baz :: Lens' (Foo a) Int
quux :: Lens (Foo a) (Foo b) a bOne sentence from the README explains the split: "A `Lens` takes 4 parameters because it can change the types of the whole when you change the type of the part." Often "you won't need this flexibility, a `Lens'` takes 2 parameters, and can be used directly as a `Lens`."
Each field name in the record has a leading prefix. Each generated optic name has that prefix removed. Field `_bar` produces optic bar, `_baz` produces baz, and `_quux` produces `quux`. `makePrisms` covers sum types and newtypes by the same notation, as the `makePrisms ''Neither` call in the next section shows.
Composition joins two optics with (.) from the Prelude and reads left to right
Composition in this library "just uses `(.)` from the `Prelude`". Two lenses chain with a period, and the leftmost applies first:
ghci> ("hello",("world","!!!"))^._2._1
"world"For extending a chain with a pure function: "You can make a `Getter` out of a pure function with `to`."
ghci> ("hello",("world","!!!"))^._2._2.to length
3That result is 3 because `"!!!"` has 3 characters. Writing to a nested position uses the same chained form:
ghci> set (_2._1) 42 ("hello",("world","!!!"))
("hello",(42,"!!!"))`both *~ 2` multiplies each element of a pair:
ghci> both *~ 2 $ (1,2)
(2,4)Applying `_1.mapped._2.mapped %~ succ` to a nested structure:
ghci> _1.mapped._2.mapped %~ succ $ ([(42, "hello")],"world")
([(42, "ifmmp")],"world")Four optics connect by (.) in that single chain, reaching the characters of the string inside a list inside the first element of the outer pair.
makePrisms and makeLenses both derive an isomorphism from Neither, in opposite directions
For a newtype over `Either`:
newtype Neither a b = Neither { _nor :: Either a b } deriving (Show)
makePrisms ''Neithergenerates:
_Neither :: Iso (Neither a b) (Neither c d) (Either a b) (Either c d)Applying `makeLenses` to the same declaration:
makeLenses ''Neithergenerates:
nor :: Iso (Either a b) (Either c d) (Neither a b) (Neither c d)The type constructor in the declaration is Neither. The generated optic from makePrisms is `_Neither`: Neither with a leading prefix added. The record field in the declaration is `_nor`; makeLenses uses it to produce nor, dropping the leading prefix from the field name. The type constructor Neither produces `_Neither`; the field `_nor` produces nor. Both macros start from the same data declaration and produce isomorphisms, but at different type positions. Comparing the 2 signatures reveals what the README's following claim skips over.
The Neither and nor isomorphisms have their first type position reversed
Both signatures from README.markdown side by side:
_Neither :: Iso (Neither a b) (Neither c d) (Either a b) (Either c d)
nor :: Iso (Either a b) (Either c d) (Neither a b) (Neither c d)In _Neither, the first type is Neither a b; in nor, the first type is Either a b. Their positions are swapped. The README then states that nor "behaves identically to `_Neither`." That claim describes what both isomorphisms can do: reading and writing in either direction through `from`. The README demonstrates the round-trip: `_Neither.from _Neither = id` and `from _Neither._Neither = id`.
The README also shows `packed` as an isomorphism between String and Text, and gives this example of `from` reversing direction in a chain:
ghci> let hello = "hello"^.packed
ghci> hello^.from packed.to length
5A chain starting from a Neither value uses _Neither directly; a chain starting from an Either value uses nor directly. What makes the README's use of "identically" accurate is that isomorphisms are always invertible via `from`, regardless of which direction the type signature was written.
Folds and Traversals sit in composed optic chains alongside Lens
`mapMOf` applies monadic actions through a Traversal the way `mapM` applies them through a plain list. `allOf` checks predicates through a Fold. Both accept composed optic chains. An example with `mapMOf`:
ghci> mapMOf (traverse._2) (\xs -> length xs <$ putStrLn xs) [(42,"hello"),(56,"world")]
"hello"
"world"
[(42,5),(56,5)]`traverse._2` composes by (.) with any other optic in the same chain.
Neil Mitchell's `uniplate` library is the credited origin for the `biplate` combinators, which the README says "have been generalized to work on or as lenses, folds, and traversals." `anyOf biplate` searches matching sub-terms in a nested structure:
ghci> anyOf biplate (=="world") ("hello",(),[(2::Int,"world")])
TrueUsing `uniplate` directly covers generic traversal alone, without (.) composition alongside other optic types.
That README body is truncated before completing a section on further features. A field guide at images/Hierarchy.png and the Lens Wiki cover what the README body does not reach.
State monad and arithmetic operators use the same optic types as read and write
Beyond static access, the library includes State monad combinators. A representative example from the README:
fresh :: MonadState Int m => m Int
fresh = id <+= 1In that example, `id <+= 1` reads the current state value, returns it, and then increments the state by 1. There are a number of combinators that resemble the +=, *=, etc. operators from C/C++ for working with the monad transformers. There are +~, *~, etc. analogues to those combinators that work functionally, returning the modified version of the structure.
For a Fold across Text values, the README shows:
ghci> :m + Data.Char Data.Text.Lens
ghci> allOf (folded.text) isLower ["hello"^.packed, "goodbye"^.packed]
True`folded.text` joins a fold over a list with a lens into a Text value, and `allOf` checks the predicate across every element reached by that chain. `packed` appears here as an isomorphism from Data.Text.Lens, the same lens used in the `from packed` example in the previous section.
The last push was on 2026-10-04 and the repository lists no GitHub releases. The licence is a custom text in the LICENSE file at the repository root.
Editorial conclusion
For Haskell programmers working with nested records, `makeLenses ''Foo` produces a typed optic for each field. .~ is an infix alias for set, (.) from the Prelude composes any 2 optics, and `Lens'` can be used directly as a `Lens` when the type does not change. Before adopting the library, run the README's first ghci session against a record type from your own codebase to check which generated optic names appear. For the full hierarchy, read the Lens Wiki FAQ and the derivation overview linked from the README, and check the field guide image at images/Hierarchy.png in the repository.
Frequently asked questions
How do I add the lens library to a Haskell project?
lens is published on Hackage at hackage.haskell.org/package/lens. Add it as a dependency in your Cabal file and use import Control.Lens to access the core API.
What is the difference between Lens and Lens' in the lens library?
A Lens takes 4 type parameters and covers writes that change the type of the container. Lens' takes 2 parameters for the case where the type does not change. Lens' can be used directly as a Lens.
What macros does the lens library provide for deriving optics?
makeLenses derives optics from record field names beginning with a leading prefix. makePrisms generates isomorphisms for sum types and newtypes. The README examples show both applied to concrete data types.
How do Fold and Traversal relate to Foldable and Traversable in this library?
A Fold covers inspection tasks the way Foldable does. A Traversal covers effectful updates the way traverse does. Both accept composed optic chains as their target rather than requiring a Foldable or Traversable instance directly.
What naming convention does makeLenses require in the lens library?
Field names must begin with a leading prefix. In the README example, fields _bar, _baz and _quux produce optics bar, baz and quux with the leading prefix removed from each generated name.