Haskell Lens Composition
A composed lens is a lens into a field of a field. It is built from an outer lens and an inner lens, outer first.
A composed getter reads through two levels
getVal first reads the inner record from the outer record. Then it reads the value from the inner record.
data Inner = Inner { innerVal :: Int } deriving Show
data Outer = Outer { outerInner :: Inner, outerTag :: String } deriving Show
getInnerVal :: Inner -> Int
getInnerVal (Inner v) = v
getOuterInner :: Outer -> Inner
getOuterInner (Outer i _) = i
getVal :: Outer -> Int
getVal = getInnerVal . getOuterInner
main :: IO ()
main = print (getVal (Outer (Inner 7) "home"))A composed setter rebuilds both levels
setVal first rebuilds the inner record with the new value. Then it rebuilds the outer record around that inner record.
data Inner = Inner { innerVal :: Int } deriving Show
data Outer = Outer { outerInner :: Inner, outerTag :: String } deriving Show
getOuterInner :: Outer -> Inner
getOuterInner (Outer i _) = i
setInnerVal :: Int -> Inner -> Inner
setInnerVal v (Inner _) = Inner v
setOuterInner :: Inner -> Outer -> Outer
setOuterInner i (Outer _ t) = Outer i t
setVal :: Int -> Outer -> Outer
setVal v o = setOuterInner (setInnerVal v (getOuterInner o)) o
main :: IO ()
main = print (setVal 9 (Outer (Inner 7) "home"))A composed lens can update a nested field
The composed getter reads 7, adds one, and the composed setter writes 8 back. The tag stays unchanged.
data Inner = Inner { innerVal :: Int } deriving Show
data Outer = Outer { outerInner :: Inner, outerTag :: String } deriving Show
getInnerVal :: Inner -> Int
getInnerVal (Inner v) = v
getOuterInner :: Outer -> Inner
getOuterInner (Outer i _) = i
setInnerVal :: Int -> Inner -> Inner
setInnerVal v (Inner _) = Inner v
setOuterInner :: Inner -> Outer -> Outer
setOuterInner i (Outer _ t) = Outer i t
getVal :: Outer -> Int
getVal = getInnerVal . getOuterInner
setVal :: Int -> Outer -> Outer
setVal v o = setOuterInner (setInnerVal v (getOuterInner o)) o
main :: IO ()
main = print (setVal (getVal (Outer (Inner 7) "home") + 1) (Outer (Inner 7) "home"))Example
data Inner = Inner { innerVal :: Int } deriving Show
data Outer = Outer { outerInner :: Inner, outerTag :: String } deriving Show
getInnerVal :: Inner -> Int
getInnerVal (Inner v) = v
getOuterInner :: Outer -> Inner
getOuterInner (Outer i _) = i
setInnerVal :: Int -> Inner -> Inner
setInnerVal v (Inner _) = Inner v
setOuterInner :: Inner -> Outer -> Outer
setOuterInner i (Outer _ t) = Outer i t
getVal :: Outer -> Int
getVal = getInnerVal . getOuterInner
setVal :: Int -> Outer -> Outer
setVal v o = setOuterInner (setInnerVal v (getOuterInner o)) o
main :: IO ()
main = do
print (setVal 42 (Outer (Inner 7) "home"))
print (getVal (setVal 42 (Outer (Inner 7) "home")))Key points
- Composition: joining two lenses so one step reaches a nested part.
- Outer lens: the lens that reaches the inner record.
- Inner lens: the lens that reaches the field inside that record.
- Getter order: the composed getter reads through the outer lens and then the inner lens.
- Setter order: the composed setter rebuilds the inner record and then the outer record.
- Use composition to reach a nested field with one lens instead of two steps.
In this example, setVal first replaces the nested value with 42 while keeping the tag, then getVal reads the nested value back from the rebuilt outer record.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Haskell Playground
Exercise
For a composed lens, which lens is applied first when building it?