Firstly, what is lens/lenses/optics?
Imagine a nested data structure:
data Address = Address
{ addressCity :: !Text
, addressStreet :: !Text
}
data Person = Person
{ personAddress :: !Address
, personName :: !Text
}
If you have a value alice :: Person
, and you want to get the
person’s city, you can use record accessors as normal functions:
getPersonCity :: Person -> Text
getPersonCity = addressCity . personAddress
alice'sCity :: Text
alice'sCity = getPersonCity alice
That’s pretty elegant. But let’s say that you want to change Alice’s city to something else. In a mutable, object-oriented language, you’d probably expect something like:
alice.address.city = "Los Angeles";
The first issue in Haskell is that we can’t mutate alice
; we instead
have to return a new Person
value with the updated city. Type
signature wise, we’d be looking at:
setPersonCity :: Text -> Person -> Person
That makes sense. Now let’s see how we’d implemente this:
import Data.Text (Text)
data Address = Address
{ addressCity :: !Text
, addressStreet :: !Text
}
data Person = Person
{ personAddress :: !Address
, personName :: !Text
}
getPersonCity :: Person -> Text
getPersonCity = addressCity . personAddress
setPersonCity :: Text -> Person -> Person
setPersonCity city person = person
{ personAddress = (personAddress person)
{ addressCity = city
}
}
Well… that obviously sucks. It only gets worse as the nesting levels go deeper. Let’s look at some ways to make this easier to stomach.
Let’s see if we can make this slightly less painful with some modifier functions:
modifyAddressCity :: (Text -> Text) -> Address -> Address
modifyAddressCity f address = address
{ addressCity = f (addressCity address)
}
modifyPersonAddress :: (Address -> Address) -> Person -> Person
modifyPersonAddress f person = person
{ personAddress = f (personAddress person)
}
modifyPersonCity :: (Text -> Text) -> Person -> Person
modifyPersonCity = modifyPersonAddress . modifyAddressCity
setPersonCity :: Text -> Person -> Person
setPersonCity city = modifyPersonCity (const city)
Composing the modifier functions works nicely, and then we can easily convert a modifier function into a setter function. Writing the initial modifier functions is tedious, but that’s the price of doing business.
Another downside is that we’ve totally separated out the getter and modifier functions. Let’s see if we can combine those.
If our problem is splitting up the getters and modifiers, let’s just stick them together.
data Lens s a = Lens
{ lensGetter :: s -> a
, lensModify :: (a -> a) -> s -> s
}
Previously we could compose our getters and modifiers with the good
old .
function composition operator, but now we need something a bit
more specialized:
composeLens :: Lens a b -> Lens b c -> Lens a c
composeLens (Lens getter1 modify1) (Lens getter2 modify2) = Lens
{ lensGetter = getter2 . getter1
, lensModify = modify1 . modify2
}
With that in hand, we can write lenses for an address’s city, a person’s address, put them together, and then easily extract a setter:
personAddressL :: Lens Person Address
personAddressL = Lens
{ lensGetter = personAddress
, lensModify = f person -> person { personAddress = f (personAddress person) }
}
addressCityL :: Lens Address Text
addressCityL = Lens
{ lensGetter = addressCity
, lensModify = f address -> address { addressCity = f (addressCity address) }
}
personCityL :: Lens Person Text
personCityL = personAddressL `composeLens` addressCityL
setPersonCity :: Text -> Person -> Person
setPersonCity city = lensModify personCityL (const city)
This works, but it feels clunky. It also has some performance overhead
we didn’t have previously due to the creation of the Lens
values. And a more advanced topic we haven’t even touched on yet: it
doesn’t allow for polymorphic update, which deals with changing type
variables (we won’t deal with that for now).
In all honesty, understanding exactly how these next forms of lenses work isn’t strictly necessary. It’s built on the same premise as the previous kinds of lenses, but it’s:
Let’s start slowly in motivating this. Our first goal is to see if we can combine our getter and modifier into a single value, without using a product type. We need to be able to extract both a getter and modifier from this value, so it has to provide the following:
type Lens s a = ?
view :: Lens s a -> s -> a
view = ?
over :: Lens s a -> (a -> a) -> s -> s
over = ?
(Ignore the funny names, they’re part of lens
.)
It doesn’t seem like those two output types (s -> a
and (a -> a) -> s -> s
) have much in common. But we’re going to use a trick to make
them match up. Let’s start with the over
result:
(a -> a) -> (s -> s)
I’m going to wrap up the results of the two functions inside the
Identity
functor:
newtype Identity a = Identity { runIdentity :: a }
deriving Functor
type LensModify s a = (a -> Identity a) -> (s -> Identity s)
over :: LensModify s a -> (a -> a) -> s -> s
over lens f s = runIdentity (lens (Identity . f) s)
And we can create values for this lens type with:
personAddressL :: LensModify Person Address
personAddressL f person = Identity $ person
{ personAddress = runIdentity $ f $ personAddress person
}
Or, if we want to take advantage of the Functor
instance and not
play with wrapping and unwrapping the Identity
values, we get:
personAddressL :: LensModify Person Address
personAddressL f person =
(address -> person { personAddress = address })
<$> f (personAddress person)
Alright, let’s switch over to the getter side. This time around, we
want to start with the same basic (a -> a) -> (s -> s)
, but apply a
different wrapper type to allow us to get a getter function at the
end, s -> a
. So in other words, we need to be able to convert from
s -> Wrapper s
to s -> a
. This may seem impossible at first, but
it turns out that there’s a cool trick to make this happen:
newtype Const a b = Const { getConst :: a }
deriving Functor
type LensGetter s a = s -> Const a s
view :: LensGetter s a -> s -> a
view lens s = getConst (lens s)
personAddressL :: LensGetter Person Address
personAddressL person = Const (personAddress person)
This is fairly complex. Const
is a data type that does the same
thing as the const
function: it holds onto one value and ignores a
second. Here, Const
is keeping our Address
value for us and
allowing use to extract it inside view
. Stare at it a while and it
will make sense, but it’s also just a convoluted way to get to our
goal.
Ultimately, our goal is to make LensGetter
and LensModify
the same
thing. But right now, they don’t look very similar.
type LensModify s a = (a -> Identity a) -> (s -> Identity s)
type LensGetter s a = s -> Const a s
In order to bring them more inline, we need to redefine LensGetter
as:
type LensGetter s a = (a -> Const a s) -> (s -> Const a s)
And it turns out by shuffling around some things just a bit, we can make this work as well:
type LensGetter s a = (a -> Const a a) -> (s -> Const a s)
view :: LensGetter s a -> s -> a
view lens s = getConst (lens Const s)
personAddressL :: LensGetter Person Address
personAddressL f person = Const $ getConst $ f (personAddress person)
Again, kind of crazy, but it works. Our wrapper type is now Const a
,
and we pass in the Const
data constructor to the lens
. It all
kinda sorta works. One final tweak on all of this. Previously, we
defined our modify lens using the Functor
interface so we didn’t
need to know about Identity
at all:
personAddressL :: LensModify Person Address
personAddressL f person =
(address -> person { personAddress = address })
<$> f (personAddress person)
It turns out that this exact same function body works for defining
LensGetter
:
personAddressL :: LensGetter Person Address
personAddressL f person =
(address -> person { personAddress = address })
<$> f (personAddress person)
And now we can finally unify our getter and modify lenses into one:
type Lens s a = forall f. Functor f => (a -> f a) -> (s -> f s)
newtype Identity a = Identity { runIdentity :: a }
deriving Functor
newtype Const a b = Const { getConst :: a }
deriving Functor
over :: Lens s a -> (a -> a) -> s -> s
over lens f s = runIdentity (lens (Identity . f) s)
view :: Lens s a -> s -> a
view lens s = getConst (lens Const s)
personAddressL :: Lens Person Address
personAddressL f person =
(address -> person { personAddress = address })
<$> f (personAddress person)
getPersonAddress :: Person -> Address
getPersonAddress = view personAddressL
modifyPersonAddress :: (Address -> Address) -> Person -> Person
modifyPersonAddress = over personAddressL
setPersonAddress :: Address -> Person -> Person
setPersonAddress address = modifyPersonAddress (const address)
This means that we have a lens if we support all possible functors in that type signature. It turns out, almost as if by magic, that this allows us to recapture getter and modifier functions (and via modifier, setter functions).
The formulation is wonky, and very difficult to grasp. Don’t worry if the intuition hasn’t kicked in. It turns out that you can use lenses quite a bit without fully grokking them.
First, let’s define a helper function for turning a getter and a setter into a lens:
lens :: (s -> a) -> (s -> a -> s) -> Lens s a
lens getter setter = f s -> setter s <$> f (getter s)
Then we can more easily define lenses for person.address
and
address.city
:
personAddressL :: Lens Person Address
personAddressL = lens personAddress (x y -> x { personAddress = y })
addressCityL :: Lens Address Text
addressCityL = lens addressCity (x y -> x { addressCity = y })
How do we compose them together into the person.address.city
lens?
If we expand the type signatures a bit it may become obvious:
personAddressL :: Functor f => (Address -> f Address) -> (Person -> f Person)
addressCityL :: Functor f => (Text -> f Text) -> (Address -> f Address)
personCityL :: Functor f => (Text -> f Text) -> (Person -> f Person)
How would you implement personCityL
? Well, turns out to be really
easy:
personCityL :: Lens Person Text
personCityL = personAddressL.addressCityL
This is a great strength of lenses: they work with normal function composition. They also work in what Haskellers would consider backwards order: the composition seems to flow from left to right instead of right to left. But on the other hand, this seems to match up perfectly with what object oriented developers expect, so that’s nice.
Dealing directly with the Lens
type is needlessly painful. Instead,
we work through helper functions and operators. You’ve already seen
view
, over
, and lens
. Let’s implement a few more:
(^.) :: s -> Lens s a -> a
s ^. lens = view lens s
infixl 8 ^.
(%~) :: Lens s a -> (a -> a) -> s -> s
(%~) = over
infixr 4 %~
reverseCity :: Person -> Person
reverseCity = personAddressL.addressCityL %~ T.reverse
getCity :: Person -> Text
getCity person = person ^. personAddressL.addressCityL
set :: Lens s a -> a -> s -> s
set lens a s = runIdentity $ lens (_olda -> Identity a) s
setCity :: Text -> Person -> Person
setCity = set (personAddressL.addressCityL)
It turns out that we can make lenses a bit more general. If we look at the current type:
type Lens s a = forall f. Functor f => (a -> f a) -> (s -> f s)
It requires that the field originally be of type a
and ultimately of
type a
. It also requires that the value overall is of type s
and
ultimately of type s
. Let’s create a new datatype where this would
be limiting:
data Person age = Person
{ personName :: !Text
, personAge :: !age
}
aliceInt :: Person Int
aliceInt = Person "Alice" 30
personAgeL :: Lens (Person age) age
personAgeL = lens personAge (x y -> x { personAge = y })
setAge :: age -> Person oldAge -> Person age
setAge age person = person { personAge = age }
aliceDouble :: Person Double
aliceDouble = setAge 30.5 aliceInt
Try as we might, we cannot use personAgeL
to change the age value
from an Int
to a Double
. Its construction requires that the input
and output age
type variable remain the same. However, with a small
extension to our Lens
type, we can make this work:
type Lens s t a b = forall f. Functor f => (a -> f b) -> (s -> f t)
-- Our old monomorphic variant
type Lens' s a = Lens s s a a
This says that we have some data structure, s
. Inside s
is a value
a
. If you replace that a
with a b
, you get out a t
. Sound
weird? Let’s see it in practice:
personAgeL :: Lens (Person age1) (Person age2) age1 age2
personAgeL = lens personAge (x y -> x { personAge = y })
setAge :: age -> Person oldAge -> Person age
setAge = set personAgeL
What we’ve seen so far is the original motivating case. It turns out
that there are many crazy ways of generalizing a Lens
further to
represent more usages. This comes by means of various techniques, such
as:
Functor
a -> f b
) with profunctors (e.g. p a (f b)
)We’ve already seen examples of the concrete types approach. Let’s use their more standard names now:
type ASetter s t a b = (a -> Identity b) -> s -> Identity t
type ASetter' s a = ASetter s s a a
type Getting r s a = (a -> Const r a) -> s -> Const r s
type SimpleGetter s a = forall r. Getting r s a
By using different typeclasses, we’re able to create a form of
subtyping. For example, every Applicative
is also a Functor
. So if
we define a new type like this:
type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t
Every Lens s t a b
is also a Traversal s t a b
, but the reverse is
not true. We can go even deeper down the rabit hole with:
type Fold s a = forall f. (Contravariant f, Applicative f) => (a -> f a) -> s -> f s
Now we need f
to be both Applicative
and Contravariant
, so that
all Traversal
s are Fold
s, but not all Fold
s are
Traversal
s. This actually matches up with the related typeclasses
Foldable
and Traversable
, where the latter is a subclass of the
former.
But what does this have to do with field accessors? you may ask. This is what I was implying above with lens being its own language on top of Haskell: if desired, you can replace a lot of functionality found elsewhere with lens-centric code. All of these different types I’ve mentioned are known as optics, and since they all have roughly the same shape, they compose together very nicely.
The lens
package itself is fully loaded, and provides lots of
helper functions, operators, types, and generality. It also has a
relatively heavy dependency footprint. Many projects instead use
microlens
, which has a much lighter footprint, but also lacks some
functionality (for example, prisms).
If writing those lenses above by hand seems tedious to you, you’re not alone. Many people use macros/code generation/Template Haskell (all the same thing in Haskell) to automatically generate lenses, and sometimes typeclasses to generalize them. Examples:
The most important decision to be made for a team is how to use lenses. Best practices are vital. You do not want half the team using advanced lens features and the other half not understanding them at all. I can advise on what I consider best practices, but it will depend a lot on how your team wants to approach things. What I’ve standardized on:
Lens
types and its functions: solid gold, do it,
don’t hold back! The biggest downside is the slightly confusing
error messages, but you get used to that quicklylookup
) with their lensy
counterparts (like at
): not worth it. You’ll make your code
shorter, but I prefer the standard Haskell idioms to relearning with
lens.We should base the homework exercises around how deeply into lenses the team wants to go.
Fill out the stubs below to make the test suite pass. Probably goes
without saying, but: use the generated lenses (address
, street
,
age
, etc) wherever possible instead of falling back to records.
#!/usr/bin/env stack
-- stack --resolver lts-12.21 script
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
import Lens.Micro.Platform
import Data.Text (Text)
import Test.Hspec
data Address = Address
{ _street :: !Text
, _city :: !Text
}
makeLenses ''Address
data Person = Person
{ _name :: !Text
, _address :: !Address
, _age :: !Int
}
makeLenses ''Person
hollywood :: Text
hollywood = "Hollywood Blvd"
alice :: Person
alice = Person
{ _name = "Alice"
, _address = Address
{ _street = hollywood
, _city = "Los Angeles"
}
, _age = 30
}
wilshire :: Text
wilshire = "Wilshire Blvd"
aliceWilshire :: Person
aliceWilshire = _ -- FIXME set Alice's street to Wilshire
getStreet :: Person -> Text
getStreet = _
-- | Increase age by 1
birthday :: Person -> Person
birthday = _
getAge :: Person -> Int
getAge = _
main :: IO ()
main = hspec $ do
it "lives on Wilshire" $
_street (_address aliceWilshire) `shouldBe` wilshire
it "getStreet works" $
getStreet alice `shouldBe` hollywood
it "birthday" $
getAge (birthday alice) `shouldBe` _age alice + 1
Remove the {-# LANGUAGE TemplateHaskell #-}
line from the previous
exercise and get the code to compile. You’ll need to define your own
lenses to make this work. Use the lens
helper function, and make
sure to add type signatures to the values you create.
Real challenge: now implement those lenses again, but without using
the lens
helper function. Instead, use fmap
or <$>
directly.
There are tuple lenses provided, named _1
, _2
, and so on, for
modifying components in tuples. Fill in the stub below so that the
test passes:
#!/usr/bin/env stack
-- stack --resolver lts-12.21 script
{-# LANGUAGE OverloadedStrings #-}
import Lens.Micro.Platform
import Test.Hspec
main :: IO ()
main = hspec $
it "fun with tuples" $
let tupleLens = _
tuple :: ((Int, Double), (Bool, Char, String))
tuple = ((1, 2), (True, 'x', "Hello World"))
in over tupleLens not tuple `shouldBe`
((1, 2), (False, 'x', "Hello World"))
Everything we’ve done so far has been on product types. In these cases, lenses work perfectly: we know that we have every field available. However, lenses do not work perfectly on sum types, where values may or may not exist. In these cases, prisms, traversals, and folds come into play. We’re not necessarily going to be using these in depth, but it’s good to be aware of them.
Let’s use the _Left
and _Right
prisms (which work as traversals
and folds as well). Fill in the expected values for the test suite
below to begin to get an intuition for how the traversal functions
work.
#!/usr/bin/env stack
-- stack --resolver lts-12.21 script
import Lens.Micro.Platform
import Test.Hspec
main :: IO ()
main = hspec $ do
it "over left on left" $
let val :: Either Int Double
val = Left 5
in over _Left (+ 1) val `shouldBe` _
it "over left on right" $
let val :: Either Int Double
val = Right 5
in over _Left (+ 1) val `shouldBe` _
it "set left on left" $
let val :: Either Int Double
val = Left 5
in set _Left 6 val `shouldBe` _
it "set left on right" $
let val :: Either Int Double
val = Right 5
in set _Left 6 val `shouldBe` _
Bonus! This one makes more extreme usage of the folds and traversals. Reimplement some common library functions using lenses. This will require looking through the docs for microlens or lens quite a bit.
#!/usr/bin/env stack
-- stack --resolver lts-12.21 script
{-# OPTIONS_GHC -Wall -Werror #-}
{-# LANGUAGE RankNTypes #-}
import Lens.Micro.Platform
import Test.Hspec
import Data.Monoid (Endo)
-- | map/fmap
mapLens :: ASetter s t a b -> (a -> b) -> s -> t
mapLens = _
-- | toList
toListLens :: Getting (Endo [a]) s a -> s -> [a]
toListLens = _
-- | catMaybes
catMaybesLens :: [Maybe a] -> [a]
catMaybesLens = _
main :: IO ()
main = hspec $ do
it "mapLens" $
mapLens _2 not ((), True) `shouldBe` ((), False)
it "toListLens" $
toListLens both ('x', 'y') `shouldBe` "xy"
it "catMaybesLens" $
catMaybesLens [Just 'x', Nothing, Just 'y'] `shouldBe` "xy"
Note that there are many other ways to solve some of these problems.
#!/usr/bin/env stack
-- stack --resolver lts-12.21 script
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
import Lens.Micro.Platform
import Data.Text (Text)
import Test.Hspec
data Address = Address
{ _street :: !Text
, _city :: !Text
}
makeLenses ''Address
data Person = Person
{ _name :: !Text
, _address :: !Address
, _age :: !Int
}
makeLenses ''Person
hollywood :: Text
hollywood = "Hollywood Blvd"
alice :: Person
alice = Person
{ _name = "Alice"
, _address = Address
{ _street = hollywood
, _city = "Los Angeles"
}
, _age = 30
}
wilshire :: Text
wilshire = "Wilshire Blvd"
aliceWilshire :: Person
aliceWilshire = set (address.street) wilshire alice
getStreet :: Person -> Text
getStreet = view (address.street)
--getStreet = (^. address.street)
-- | Increase age by 1
birthday :: Person -> Person
birthday = over age (+ 1)
--birthday = age %~ (+ 1)
getAge :: Person -> Int
getAge = view age
main :: IO ()
main = hspec $ do
it "lives on Wilshire" $
_street (_address aliceWilshire) `shouldBe` wilshire
it "getStreet works" $
getStreet alice `shouldBe` hollywood
it "birthday" $
getAge (birthday alice) `shouldBe` _age alice + 1
street :: Lens' Address Text
street = lens _street (x y -> x { _street = y })
address :: Lens' Person Address
address = lens _address (x y -> x { _address = y })
age :: Lens' Person Int
age = lens _age (x y -> x { _age = y })
street :: Lens' Address Text
street f address = (x -> address { _street = x }) <$> f (_street address)
address :: Lens' Person Address
address f person = (x -> person { _address = x }) <$> f (_address person)
age :: Lens' Person Int
age f person = (x -> person { _age = x }) <$> f (_age person)
let tupleLens = _2._1
The most important bit here to notice: using set _Left
did not
change the data constructor from Right
to Left
.
#!/usr/bin/env stack
-- stack --resolver lts-12.21 script
import Lens.Micro.Platform
import Test.Hspec
main :: IO ()
main = hspec $ do
it "over left on left" $
let val :: Either Int Double
val = Left 5
in over _Left (+ 1) val `shouldBe` Left 6
it "over left on right" $
let val :: Either Int Double
val = Right 5
in over _Left (+ 1) val `shouldBe` Right 5
it "set left on left" $
let val :: Either Int Double
val = Left 5
in set _Left 6 val `shouldBe` Left 6
it "set left on right" $
let val :: Either Int Double
val = Right 5
in set _Left 6 val `shouldBe` Right 5
#!/usr/bin/env stack
-- stack --resolver lts-12.21 script
{-# OPTIONS_GHC -Wall -Werror #-}
{-# LANGUAGE RankNTypes #-}
import Lens.Micro.Platform
import Test.Hspec
import Data.Monoid (Endo)
-- | map/fmap
mapLens :: ASetter s t a b -> (a -> b) -> s -> t
mapLens l f = over l f
-- | toList
toListLens :: Getting (Endo [a]) s a -> s -> [a]
toListLens l s = s ^.. l
-- | catMaybes
catMaybesLens :: [Maybe a] -> [a]
catMaybesLens list = list ^.. each._Just
main :: IO ()
main = hspec $ do
it "mapLens" $
mapLens _2 not ((), True) `shouldBe` ((), False)
it "toListLens" $
toListLens both ('x', 'y') `shouldBe` "xy"
it "catMaybesLens" $
catMaybesLens [Just 'x', Nothing, Just 'y'] `shouldBe` "xy"
Subscribe to our blog via email
Email subscriptions come from our Atom feed and are handled by Blogtrottr. You will only receive notifications of blog posts, and can unsubscribe any time.