Lenses are a popular and powerful technique for working with nested data structures in Haskell. They provide a composable, functional way to get, set, and modify values within deeply nested data structures. The primary advantage of lenses is that they make it easier to reason about transformations on data structures and help avoid boilerplate code.
The ‘lens‘ library (https://hackage.haskell.org/package/lens) is the most popular and extensive implementation of lenses in Haskell.
Consider the following example with nested records:
data Address = Address
{ street :: String
, city :: String
, country :: String
} deriving (Show)
data Person = Person
{ name :: String
, age :: Int
, address :: Address
} deriving (Show)
Suppose we have a ‘Person‘ instance and want to change the street name:
p1 :: Person
p1 = Person "Alice" 30 (Address "Main Street" "New York" "USA")
Without lenses, we might do something like the following:
changeStreet :: String -> Person -> Person
changeStreet newStreet p =
p { address = (address p) { street = newStreet } }
Let’s use the lens library (make sure you have ‘lens‘ package installed by adding ‘lens‘ to your dependencies in your ‘.cabal‘ or ‘package.yaml‘ file) to make this task easier:
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
-- Use TemplateHaskell to auto-generate lenses
makeLenses ''Address
makeLenses ''Person
-- Now we can change the street easily using lenses
p2 :: Person
p2 = p1 & address . street .~ "New Street"
Here, ‘makeLenses‘ is a Template Haskell function that generates lenses for the named records. For each field, it creates a lens with the same name (there exist other naming schemes as well, like ‘makeFields‘ for automatic lenses generation based on the type). The operators we used are:
- ‘(&)‘: reverse function application (‘x & f == f x‘),
- ‘(.)‘: lens composition
- ‘(. )‘: set a value using a lens
For instance, in ‘p1 & address . street . "New Street"`, we’re saying: "Set the value of the ‘street‘ field of the ‘address‘ field in the record ‘p1‘ to ’New Street’".
Additionally, lenses provide many more capabilities like reading and modifying nested fields. Here are some more examples:
- Get the street of a person:
streetName :: String
streetName = p1 ^. address . street
Here, ‘(.̂)‘ retrieves the value of the specified lens.
- Increment the age of a person by one (using ‘over‘ to apply a function to a lens target):
p3 :: Person
p3 = over (age) (+1) p1
- Set the city of a person:
p4 :: Person
p4 = p1 & address . city .~ "Los Angeles"
To sum up, lenses provide a composable, functional way to work with nested data structures in Haskell. The ‘lens‘ library offers a powerful set of utilities to make it easy to get, set, and modify values within those structures.