Project 11: Lenses and Optics Library
Project 11: Lenses and Optics Library
Conquering Nested Data with Composable Accessors
- Main Programming Language: Haskell
- Alternative Languages: Scala (Monocle), PureScript, TypeScript (monocle-ts)
- Coolness Level: Level 4: Hardcore Tech Flex
- Difficulty: Level 4: Expert
- Knowledge Area: Data Access / Optics / Category Theory
- Estimated Time: 3-4 weeks
- Prerequisites: Projects 5 (Algebraic Data Types), Project 6 (Functor/Applicative/Monad), comfort with higher-order functions
Learning Objectives
After completing this project, you will be able to:
- Implement the Lens type from first principles - Build lenses as pairs of getters and setters, then upgrade to Van Laarhoven representation
- Understand why lenses compose with function composition - Explain the type-level magic that makes
(.)work for lenses - Implement the optics hierarchy - Build Lens, Prism, Traversal, Iso, and understand their relationships
- Use different functors for different operations - See how
Constextracts values andIdentityupdates them - Apply lenses to real-world data transformation - Handle deeply nested JSON, configuration, and domain models
- Connect lenses to category theory - Understand profunctor optics and the deeper mathematical structure
- Design composable data access patterns - Build libraries that are both powerful and ergonomic
Conceptual Foundation
The Problem: Updating Nested Immutable Data
In imperative programming, updating a field in a nested object is trivial:
// JavaScript: easy peasy
person.address.city = "New York";
But in pure functional programming, data is immutable. You canât mutate; you must create new values. The naive approach is painful:
-- Haskell: painful boilerplate
data Person = Person { _name :: String, _address :: Address }
data Address = Address { _city :: String, _street :: String }
updateCity :: String -> Person -> Person
updateCity newCity person =
person { _address = (_address person) { _city = newCity } }
This gets exponentially worse with deeper nesting:
-- Three levels deep...
updateZipCode :: String -> Company -> Company
updateZipCode newZip company =
company {
_headquarters = (_headquarters company) {
_location = (_location (_headquarters company)) {
_zipCode = newZip
}
}
}
-- Nightmare fuel!
Lenses solve this problem elegantly.
What Is a Lens?
At its core, a lens is a first-class accessorâa value that represents the ability to focus on a part of a larger structure. It combines:
- A getter: Extract the focused value from the whole
- A setter: Replace the focused value in the whole
-- Simple lens representation
data Lens' s a = Lens'
{ view :: s -> a -- Get the 'a' from an 's'
, set :: a -> s -> s -- Set the 'a' in an 's', return new 's'
}
With this, we can create lenses for each field:
address :: Lens' Person Address
address = Lens' _address (\newAddr person -> person { _address = newAddr })
city :: Lens' Address String
city = Lens' _city (\newCity addr -> addr { _city = newCity })
Why Lenses Must Compose
The key insight is that lenses should compose like functions. If you have:
- A lens from
PersontoAddress - A lens from
AddresstoCity
You should be able to compose them to get a lens from Person to City:
personCity :: Lens' Person String
personCity = address `composeLens` city
Letâs implement composition for our simple lens:
composeLens :: Lens' a b -> Lens' b c -> Lens' a c
composeLens (Lens' viewAB setAB) (Lens' viewBC setBC) = Lens'
{ view = viewBC . viewAB -- Get B from A, then C from B
, set = \c a ->
let b = viewAB a -- Get current B from A
b' = setBC c b -- Update C in B, get new B
in setAB b' a -- Update B in A, get new A
}
This works! But notice the complexityâweâre doing the work of composition manually. Thereâs a better way.
The Van Laarhoven Representation: Where Magic Happens
In 2009, Twan van Laarhoven discovered an encoding of lenses that makes composition automatic. Instead of storing getter and setter separately, represent a lens as a single higher-order function:
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type Lens' s a = Lens s s a a
What does this type mean?
Read it as: âGiven a way to wrap and transform the focused part (a -> f b), produce a way to wrap and transform the whole (s -> f t).â
The forall f. Functor f => means this works for any functor f. This universality is the key to its power.
Building a Van Laarhoven Lens
-- The lens constructor
lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens getter setter = \f s -> setter s <$> f (getter s)
-- |___________|
-- Update the structure with the new focused value
-- |__________|
-- Apply f to the focused value
Letâs trace through this:
getter sextracts the focused value fromsf (getter s)wraps/transforms it using the functorsetter s <$> ...maps the setter over the result, producingf t
Example:
_address :: Lens' Person Address
_address = lens _address (\person newAddr -> person { _address = newAddr })
_city :: Lens' Address String
_city = lens _city (\addr newCity -> addr { _city = newCity })
Why Composition Becomes Free
Hereâs the magic: with Van Laarhoven encoding, lens composition is just function composition!
_personCity :: Lens' Person String
_personCity = _address . _city
How does this work? Letâs trace the types:
_address :: (Address -> f Address) -> Person -> f Person
_city :: (String -> f String) -> Address -> f Address
-- Composing with (.)
_address . _city :: (String -> f String) -> Person -> f Person
When you compose _address . _city:
_citytakes(String -> f String)and produces(Address -> f Address)_addresstakes(Address -> f Address)and produces(Person -> f Person)- The composition takes
(String -> f String)and produces(Person -> f Person)
Itâs a lens from Person to String!
The functor parameter threads through automatically. This is why the Van Laarhoven encoding is so elegantâit piggybacks on existing function composition.
Using Different Functors for Different Operations
The forall f. Functor f => lets us choose different functors for different purposes:
Getting with Const
newtype Const r a = Const { getConst :: r }
instance Functor (Const r) where
fmap _ (Const r) = Const r -- Ignores the function, keeps the value
To get a value through a lens:
view :: Lens' s a -> s -> a
view l s = getConst (l Const s)
Trace: l Const s applies the lens with Const as the functor:
lcallsf (getter s)wheref = ConstConst (getter s)wraps the focused valuesetter s <$> Const (getter s)=Const (getter s)(becausefmaponConstignores the function)getConstextracts the focused value
Weâve extracted a value without ever using the setter!
Setting with Identity
newtype Identity a = Identity { runIdentity :: a }
instance Functor Identity where
fmap f (Identity a) = Identity (f a)
To set a value through a lens:
set :: Lens' s a -> a -> s -> s
set l newVal s = runIdentity (l (\_ -> Identity newVal) s)
Trace:
lreceives(\_ -> Identity newVal)as the âfocusing functionâlapplies this togetter s, ignoring the old valuesetter s <$> Identity newVal=Identity (setter s newVal)runIdentityextracts the result
Modifying with Identity
over :: Lens' s a -> (a -> a) -> s -> s
over l f s = runIdentity (l (Identity . f) s)
This applies a function to the focused value and puts it back.
The Optics Hierarchy
Lenses are just one member of a family of optics. Each focuses on different patterns:
Getter
|
Fold <---+---> Lens
| |
| +------+------+
| | |
+-> Traversal Prism
| |
+-----+-------+
|
Affine
|
|
Iso

Lens: Focus on Exactly One Value
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
- Always has exactly one target
- Can always get and set
- Example: A field in a record
Prism: Focus on Zero or One Value (Sum Types)
type Prism s t a b = forall p f. (Choice p, Applicative f) => p a (f b) -> p s (f t)
A prism is for sum types where the value might not exist:
_Left :: Prism (Either a c) (Either b c) a b
_Left = prism Left $ \case
Left a -> Right a -- Found it
Right c -> Left (Right c) -- Not here
-- Using a prism
preview _Left (Left 5) -- Just 5
preview _Left (Right 5) -- Nothing
Prisms are partial lensesâthey might fail to find a value.
Traversal: Focus on Zero or More Values
type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t
Traversals focus on multiple values at once:
-- Focus on all elements of a list
traverse :: Traversal [a] [b] a b
-- Use it to modify all elements
over traverse (*2) [1,2,3] -- [2,4,6]
-- Get all elements (as a list)
toListOf traverse [1,2,3] -- [1,2,3]
Why Applicative instead of Functor?
With multiple targets, we need to combine effects. Applicative provides <*> for this:
traverse f [] = pure []
traverse f (x:xs) = (:) <$> f x <*> traverse f xs
-- ^^^ ^^^
-- Combining two effects
Iso: Lossless Conversion
type Iso s t a b = forall p f. (Profunctor p, Functor f) => p a (f b) -> p s (f t)
An isomorphism is a lens that can go both ways with no loss:
_Reversed :: Iso' [a] [a]
_Reversed = iso reverse reverse
view _Reversed [1,2,3] -- [3,2,1]
set _Reversed [3,2,1] [1,2,3] -- [1,2,3]
Common isos:
_Wrapped :: Iso' (Identity a) a_Swapped :: Iso' (a, b) (b, a)_Chars :: Iso' String [Char]
The Profunctor Perspective
For a deeper understanding, lenses can be viewed through profunctors:
class Profunctor p where
dimap :: (a' -> a) -> (b -> b') -> p a b -> p a' b'
A profunctor is like a function that can be modified on both input and output. Regular functions form a profunctor:
instance Profunctor (->) where
dimap f g h = g . h . f -- pre-compose with f, post-compose with g
Profunctor optics generalize Van Laarhoven optics:
type Optic p s t a b = p a b -> p s t
Different constraints on p give different optics:
Profunctor p=> AdapterStrong p=> LensChoice p=> PrismTraversing p=> Traversal
This is the foundation of modern optics libraries like profunctors and optics.
Lens Laws: Guarantees of Well-Behavedness
A proper lens must satisfy three laws:
1. Get-Put (ViewSet): If you get a value and set it back, nothing changes.
set l (view l s) s == s
2. Put-Get (SetView): If you set a value and get it, you get what you set.
view l (set l a s) == a
3. Put-Put (SetSet): Setting twice is the same as setting once (with the second value).
set l a' (set l a s) == set l a' s
These laws ensure lenses behave like âproperâ accessors. Violating them leads to surprising behavior.
Example of a law-violating âlensâ:
-- Bad lens: counts how many times it's used
badLens :: IORef Int -> Lens' s a
-- Violates Put-Put: each set has side effects
Composing Different Optics
One of the beautiful properties of optics is that different types of optics compose:
-- Lens . Traversal = Traversal
-- Lens . Prism = Affine (Traversal)
-- Traversal . Traversal = Traversal
-- Prism . Lens = Affine
-- Prism . Prism = Prism
The result is always the âweakestâ of the two:
-- Focusing on the city of each employee
employeesCities :: Traversal' Company String
employeesCities = employees . traverse . address . city
-- Lens Traversal Lens Lens
-- Result: Traversal
This is why the optics hierarchy forms a latticeâcomposition moves you toward less powerful (but more general) optics.
Real-World Applications
JSON Processing
-- Deeply nested JSON
json ^. key "users" . nth 0 . key "profile" . key "email"
-- Modify deeply nested value
json & key "users" . traverse . key "status" .~ "active"
Game State
-- Game with player, inventory, equipment
data Game = Game { _player :: Player, _world :: World }
data Player = Player { _health :: Int, _inventory :: [Item] }
data Item = Item { _name :: String, _durability :: Int }
-- Give all items +10 durability
game & player . inventory . traverse . durability +~ 10
Configuration
-- Type-safe configuration access
config ^. section "database" . setting "host"
-- Modify with defaults
config & section "logging" . setting "level" %~ fromMaybe "info"
The Deep Connection to Category Theory
Lenses connect to profound mathematical structures:
Lenses as Coalgebras: A lens Lens' s a is equivalent to s -> (a, a -> s). This is a coalgebra for the costate comonad!
Profunctor Optics and Day Convolution: The full theory of profunctor optics involves concepts from category theory like Day convolution and ends/coends.
Lenses and Dependent Types: In dependently typed languages, lenses generalize to higher-order lenses that can change the type of what they focus on.
For this project, you donât need to understand all the category theory, but knowing it exists gives context for why lenses are so powerful.
Why Lenses Changed Haskell
Before lenses (pre-2012), Haskell code involving nested data was verbose and error-prone. The lens library by Edward Kmett unified years of research into a practical tool.
Today, lenses are used throughout the Haskell ecosystem:
- aeson: JSON handling with lenses
- servant: Type-safe web APIs
- reflex: Functional reactive programming
- optics: Modern reimagining of lenses
Understanding lenses is essential for production Haskell.
Project Specification
You will build an optics library from scratch. Your library should support:
Core Requirements
- Simple Lenses
Lens'type (simplified, same source and target types)- Constructor function:
lens - Operations:
view,set,over - Composition with
(.)
- Van Laarhoven Lenses
- Full
Lens s t a btype - Proper use of
forall f. Functor f => - Same operations, but more general
- Full
- Basic Optics Hierarchy
Lens: Focus on one value (Functor constraint)Traversal: Focus on multiple values (Applicative constraint)Prism: Focus on sum types (Choice constraint)Iso: Lossless conversion
- Operators
(^.)for view(.~)for set(%~)for over(&)for reverse application
- Combinators
to: Read-only lens from a functionfolded: Traversal over Foldable_1,_2: Tuple lenses_head,_tail: List lenses
Stretch Goals
- Prism construction:
prismandprism' AtandIxtype classes for container access- State monad integration:
use,.=,%= - Indexed optics: Traversals that carry indices
- TH-based lens generation (like
makeLenses)
Solution Architecture
Module Structure
src/
Optics/
Lens.hs -- Core lens types and operations
Prism.hs -- Prism type and constructors
Traversal.hs -- Traversal type and combinators
Iso.hs -- Isomorphism type
Operators.hs -- Infix operators
Combinators.hs -- Utility combinators
At.hs -- Container access type classes
Optics.hs -- Re-exports public API
Core Types
-- The lens type family
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type Lens' s a = Lens s s a a
-- Traversal: multiple targets
type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t
type Traversal' s a = Traversal s s a a
-- Prism: partial/sum type access
type Prism s t a b = forall p f. (Choice p, Applicative f) => p a (f b) -> p s (f t)
type Prism' s a = Prism s s a a
-- Iso: bidirectional conversion
type Iso s t a b = forall p f. (Profunctor p, Functor f) => p a (f b) -> p s (f t)
type Iso' s a = Iso s s a a
-- Getter: read-only
type Getter s a = forall f. (Contravariant f, Functor f) => (a -> f a) -> s -> f s
-- Fold: multiple read-only
type Fold s a = forall f. (Contravariant f, Applicative f) => (a -> f a) -> s -> f s
Helper Types
-- For extracting values (getting)
newtype Const r a = Const { getConst :: r }
-- For updating values (setting)
newtype Identity a = Identity { runIdentity :: a }
-- For profunctor optics
class Profunctor p where
dimap :: (a' -> a) -> (b -> b') -> p a b -> p a' b'
class Profunctor p => Choice p where
left' :: p a b -> p (Either a c) (Either b c)
right' :: p a b -> p (Either c a) (Either c b)
Implementation Guide
Phase 1: Simple Lenses (Days 1-3)
Goal: Implement lenses as getter/setter pairs.
Milestone 1.1: Define the simple lens type
data SimpleLens s a = SimpleLens
{ view :: s -> a
, set :: a -> s -> s
}
Milestone 1.2: Implement over
over :: SimpleLens s a -> (a -> a) -> s -> s
over l f s = set l (f (view l s)) s
Milestone 1.3: Implement composition
composeLens :: SimpleLens a b -> SimpleLens b c -> SimpleLens a c
Milestone 1.4: Create lenses for sample data types
data Person = Person { personName :: String, personAddress :: Address }
data Address = Address { addressCity :: String, addressStreet :: String }
name :: SimpleLens Person String
address :: SimpleLens Person Address
city :: SimpleLens Address String
Test: Verify get/set/over work, verify composition works.
Phase 2: Van Laarhoven Encoding (Days 4-7)
Goal: Upgrade to the Van Laarhoven representation.
Milestone 2.1: Define helper types
newtype Const r a = Const { getConst :: r }
newtype Identity a = Identity { runIdentity :: a }
instance Functor (Const r) where ...
instance Functor Identity where ...
Milestone 2.2: Define the lens type
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type Lens' s a = Lens s s a a
Milestone 2.3: Implement the lens constructor
lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens getter setter = \f s -> setter s <$> f (getter s)
Milestone 2.4: Implement view, set, over
view :: Lens' s a -> s -> a
view l s = getConst (l Const s)
set :: Lens s t a b -> b -> s -> t
set l b s = runIdentity (l (\_ -> Identity b) s)
over :: Lens s t a b -> (a -> b) -> s -> t
over l f s = runIdentity (l (Identity . f) s)
Milestone 2.5: Verify composition works with (.)
-- This should work automatically!
personCity :: Lens' Person String
personCity = address . city
Test: Same tests as Phase 1, verify (.) works.
Phase 3: Operators and Ergonomics (Days 8-10)
Goal: Add operators for fluent lens usage.
Milestone 3.1: Basic operators
(^.) :: s -> Lens' s a -> a
s ^. l = view l s
(.~) :: Lens s t a b -> b -> s -> t
l .~ b = set l b
(%~) :: Lens s t a b -> (a -> b) -> s -> t
l %~ f = over l f
(&) :: a -> (a -> b) -> b
x & f = f x
Milestone 3.2: Usage patterns
-- Fluent reading
person ^. address . city
-- Fluent updating
person & address . city .~ "Boston"
-- Fluent modifying
person & address . city %~ map toUpper
Milestone 3.3: Additional operators
(+~) :: Num a => Lens' s a -> a -> s -> s
(-~) :: Num a => Lens' s a -> a -> s -> s
(*~) :: Num a => Lens' s a -> a -> s -> s
(//~) :: Fractional a => Lens' s a -> a -> s -> s
Test: Write expressive code using operators.
Phase 4: Traversals (Days 11-14)
Goal: Implement traversals for multiple targets.
Milestone 4.1: Define the traversal type
type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t
Milestone 4.2: Implement traversal operations
toListOf :: Traversal' s a -> s -> [a]
over :: Traversal s t a b -> (a -> b) -> s -> t -- Same signature as Lens!
Milestone 4.3: Build common traversals
traverse :: Traversable t => Traversal (t a) (t b) a b
both :: Traversal (a, a) (b, b) a b
each :: Traversal [a] [b] a b
Milestone 4.4: Compose lenses and traversals
-- Lens . Traversal = Traversal
allCities :: Traversal' Company String
allCities = employees . traverse . address . city
Test: Use traversals to update multiple values.
Phase 5: Prisms (Days 15-18)
Goal: Implement prisms for sum types.
Milestone 5.1: Define profunctor classes
class Profunctor p where
dimap :: (a' -> a) -> (b -> b') -> p a b -> p a' b'
class Profunctor p => Choice p where
left' :: p a b -> p (Either a c) (Either b c)
Milestone 5.2: Define the prism type
type Prism s t a b = forall p f. (Choice p, Applicative f) => p a (f b) -> p s (f t)
Milestone 5.3: Implement prism constructor
prism :: (b -> t) -> (s -> Either t a) -> Prism s t a b
Milestone 5.4: Implement prism operations
preview :: Prism' s a -> s -> Maybe a
review :: Prism' s a -> a -> s
Milestone 5.5: Build common prisms
_Left :: Prism (Either a c) (Either b c) a b
_Right :: Prism (Either c a) (Either c b) a b
_Just :: Prism (Maybe a) (Maybe b) a b
_Nothing :: Prism' (Maybe a) ()
Test: Use prisms to work with Either and Maybe.
Phase 6: Isos and Polish (Days 19-21)
Goal: Implement isomorphisms and polish the library.
Milestone 6.1: Define the iso type
type Iso s t a b = forall p f. (Profunctor p, Functor f) => p a (f b) -> p s (f t)
Milestone 6.2: Implement iso constructor
iso :: (s -> a) -> (b -> t) -> Iso s t a b
Milestone 6.3: Implement iso operations
from :: Iso s t a b -> Iso b a t s -- Flip direction
Milestone 6.4: Build common isos
_Reversed :: Iso' [a] [a]
_Swapped :: Iso (a, b) (c, d) (b, a) (d, c)
curried :: Iso ((a, b) -> c) ((d, e) -> f) (a -> b -> c) (d -> e -> f)
Test: Use isos for bidirectional transformations.
Testing Strategy
Property-Based Tests (Using Project 10!)
-- Get-Put law
prop_getLaw :: Person -> Bool
prop_getLaw p = set _name (view _name p) p == p
-- Put-Get law
prop_putGetLaw :: Person -> String -> Bool
prop_putGetLaw p name = view _name (set _name name p) == name
-- Put-Put law
prop_putPutLaw :: Person -> String -> String -> Bool
prop_putPutLaw p n1 n2 = set _name n2 (set _name n1 p) == set _name n2 p
Unit Tests
-- Composition
test_composition = do
let person = Person "Alice" (Address "Boston" "Main St")
assertEqual "NYC" $ view (address . city) person & address . city .~ "NYC" & view (address . city)
-- Traversal
test_traversal = do
assertEqual [2,4,6] $ over traverse (*2) [1,2,3]
assertEqual [1,2,3] $ toListOf traverse [1,2,3]
Common Pitfalls
1. Forgetting the Functor Constraint
Problem: Writing a lens without the constraint:
-- WRONG
myLens :: (a -> b) -> s -> t -- Missing Functor!
Solution: Always include forall f. Functor f =>.
2. Wrong Order in Lens Composition
Problem: Composing in the wrong order:
-- WRONG: This doesn't make sense
city . address -- Types don't line up!
Solution: Compose from outer to inner: address . city.
3. Using Lens Operations on Traversals
Problem: Using view on a traversal:
view traverse [1,2,3] -- Which one? Doesn't compile!
Solution: Use toListOf or preview for traversals.
4. Creating Law-Violating Lenses
Problem: A lens that doesnât satisfy the laws:
badLens = lens getter (\_ _ -> defaultValue) -- Put-Get violation!
Solution: Always test your lenses against the lens laws.
5. Mixing Up Lens and Prism Operations
Problem: Using set on a prism expects totality:
set _Just x Nothing -- What should this return?
Solution: Use review for prisms to construct, over for modification.
Extensions and Challenges
1. Indexed Optics
Add index tracking to traversals:
itraverse :: IndexedTraversal Int [a] [b] a b
-- Access both the element and its index
2. At and Ix Type Classes
class At m where
at :: Index m -> Lens' m (Maybe (IxValue m))
class Ixed m where
ix :: Index m -> Traversal' m (IxValue m)
3. State Monad Integration
use :: Lens' s a -> State s a
(.=) :: Lens' s a -> a -> State s ()
(%=) :: Lens' s a -> (a -> a) -> State s ()
4. Template Haskell Lens Generation
makeLenses ''Person
-- Generates: name :: Lens' Person String, address :: Lens' Person Address
5. Affine Traversals
Implement 0-or-1 target optics (between Prism and Traversal).
Real-World Connections
Where Lenses Appear in Industry
- Web Development: JSON API responses with deeply nested data
- Game Development: Complex game state (player, world, inventory)
- Configuration Management: Type-safe config access
- Database ORMs: Mapping between database rows and domain types
- Financial Systems: Complex nested financial instruments
Libraries and Frameworks Using Lenses
- lens: The original, comprehensive Haskell library
- optics: Modern redesign with better type errors
- Monocle: Scalaâs optics library
- monocle-ts: TypeScript optics
- ramda: JavaScript with lens-like utilities
Interview Questions
- âWhat is a lens and why would you use one?â
- Answer: A lens is a composable accessor for nested data. In pure FP, updating nested structures without mutation is verbose. Lenses make it elegant by combining getters and setters into first-class values that compose.
- âExplain the Van Laarhoven lens representation.â
- Answer: Instead of storing getter/setter separately, represent a lens as
forall f. Functor f => (a -> f b) -> s -> f t. This makes composition automatic (just function composition) and allows different functors for different operations.
- Answer: Instead of storing getter/setter separately, represent a lens as
- âWhatâs the difference between a Lens and a Traversal?â
- Answer: A Lens focuses on exactly one value (Functor constraint). A Traversal focuses on zero or more values (Applicative constraint). A Lens is a special case of a Traversal.
- âHow do you use the Const functor with lenses?â
- Answer:
Constignores updates and just carries a value. UsingConstwith a lens extracts the focused value without modificationâthis is howviewworks.
- Answer:
- âWhat are the lens laws and why do they matter?â
- Answer: Get-Put, Put-Get, and Put-Put ensure lenses behave like proper accessors. Violating them leads to surprising behavior where setting then getting doesnât return what you set.
- âWhat is a Prism and when would you use one?â
- Answer: A Prism focuses on one branch of a sum type (like
EitherorMaybe). Use it when the target might not exist. Unlike lenses, prisms can fail to find a value.
- Answer: A Prism focuses on one branch of a sum type (like
- âHow would you implement
overfor a Van Laarhoven lens?â- Answer:
over l f s = runIdentity (l (Identity . f) s). Use the Identity functor with the modifying function to apply it to the focused value.
- Answer:
Self-Assessment Checklist
Youâve mastered this project when you can:
- Explain why nested immutable updates are painful without lenses
- Implement simple lenses with getter/setter pairs
- Upgrade to Van Laarhoven representation and explain why itâs better
- Implement
view,set, andoverusing different functors - Compose lenses using regular function composition
- Implement
ConstandIdentityfunctors correctly - Build traversals for lists and other traversable structures
- Implement prisms for sum types like
EitherandMaybe - Explain and test the lens laws
- Use operators for fluent lens syntax
- Compose different optics (lens with traversal, etc.)
- Explain the connection to category theory (at a high level)
Resources
Primary References
| Topic | Resource | Specific Section |
|---|---|---|
| Comprehensive lens guide | âOptics By Exampleâ by Chris Penner | Full book |
| Van Laarhoven encoding | âLenses, Folds, and Traversalsâ by Edward Kmett | Video/slides |
| Functor and Applicative | âHaskell Programming from First Principlesâ | Chapters 16-17 |
| Higher-order functions | âLearn You a Haskell for Great Good!â | Chapter 6 |
| Type-level programming | âThinking with Typesâ by Sandy Maguire | Chapter 6 |
Academic Papers
| Topic | Paper |
|---|---|
| Original Van Laarhoven | âCPS based functional referencesâ |
| Profunctor optics | âProfunctor Optics: Modular Data Accessorsâ |
| Algebraic optics | âCategories of Opticsâ |
Online Resources
- lens library documentation on Hackage
- âlens over teaâ blog series
- Edward Kmettâs lens talks on YouTube
- Haskell Wiki on lenses
âThe goal of a lens is to make the complex simple. Once you understand them, youâll wonder how you ever lived without them.â
After completing this project, nested data manipulation will never frustrate you again.