So last time we looked at lens and saw had to jump into record-shaped structures and change things around like big hacker professionals. However we didn’t try to change anything with a sum type
in it, like Either
or Maybe
or something. What can we use for that? Only a bloody Prism
, apparently.
Here’s an example sum type that can either contain a dog’s name or it’s age, and is in no way utterly contrived.
data DogFact = DogName String | DogAge Int
Here are some examples of it is in use. Here it is valiantly holding a dog’s name, a String
, with the DogName
constructor.
spruceBruce :: DogFact
= DogName "Spruce Bruce" spruceBruce
Great!
And here, instead, is a dog’s age, stored as an Int
inside DogAge
.
oldDog :: DogFact
= DogAge 100 oldDog
Excellent stuff, I’m sure you’ll agree.
Now, we could start making all our getters and settings by hand again like this…
getDogNameFromDogFact :: DogFact -> Maybe String
DogName s) = Just s
getDogNameFromDogFact (= Nothing
getDogNameFromDogFact _
getDogAgeFromDogFact :: DogFact -> Maybe Int
DogAge s) = Just s
getDogAgeFromDogFact (= Nothing getDogAgeFromDogFact _
…but I’m sure it’s clear it’s soon going to get quite verbose and we should probably use something more clever instead.
Enter Prism
! (the crowd goes wild, etc.)
A Prism
is like a Lens
, except it let’s you peek into a particular part of a sum type
. Therefore we can make one Prism
that is interested in the name of dogs…
dogNamePrism :: Prism' DogFact String
= prism' DogName (\e -> case e of
dogNamePrism DogName a -> Just a
-> Nothing) _
…and another which only cares about the age of dogs…
dogAgePrism :: Prism' DogFact Int
= prism' DogAge (\e -> case e of
dogAgePrism DogAge b -> Just b
-> Nothing) _
OK. So how do we use them?
In lens
, we had view
for peeking inside, set
for changing values, and over
for mapping over the values inside. What has Prism
got going on?
preview
Unlike lens
which has set
, prism
has a good pal called preview
that it uses for checking out values. It returns the result inside a Maybe
as the value is not guaranteed to actually be there.
dogAge :: Maybe Int
= preview dogAgePrism (DogAge 100)
dogAge -- dogAge == Just 100
That seems reasonable.
What about running that on spruceBruce
(that is built from a DogName
)?
notDogAge :: Maybe Int
= preview dogAgePrism spruceBruce
notDogAge -- notDogAge == Nothing
No dice. Zero dice. A complete lack of dice. No age, no int
, sorry.
Maybe our dogNamePrism
can help though…?
dogName :: Maybe String
= preview dogNamePrism spruceBruce
dogName -- dogName == Just "Spruce Bruce"
Yes! Good stuff. Does he work on ages though?
notDogName :: Maybe String
= preview dogNamePrism (DogAge 69)
notDogName -- notDogName == Nothing
No. Damn.
set
In lens
we used set
for changing the value of something nested. How does that work here?
youngerDog :: DogFact
= set dogAgePrism 27 (DogAge 100)
youngerDog -- youngerDog == DogAge 27
All seems well here, pretty similar to a regular lens
. How about we give our dog a name instead of an age?
noNewName :: DogFact
= set dogNamePrism "Nice Name" (DogAge 100)
noNewName -- noNewName == DogAge 100
What’s happened here? Where is the name? We’ve ended up with exactly what we started with! How the hell do we give the dog a name? I must admit this confused me for quite a while, until I realised that Prism
is doing exactly as it should - letting us get at the values inside a sum type without changing it’s structure.
Therefore we can change one DogName
for another…
newDogName :: dogFact
= set dogNamePrism "Excellent Bruce" (DogName "Steve")
newDogName -- newDogName == DogName "Excellent Bruce"
…but not change the name of DogAge
.
noNewDogName :: dogFact
= set dogNamePrism "Good Old Nigel" (DogAge 400)
noNewDogName -- noNewDogName == DogAge 400
over
As well as inheriting set
from lens
the prism
also has over
, that lets us map a function over nested value.
stillQuiteOldDog :: DogFact
= over dogAgePrism (-1) oldDog
stillQuiteOldDog -- stillQuiteOldDog == DogAge 99
When you think about how mapping a function doesn’t change the structure around the value (hello, functor
) then the behaviour of set
that initially confused me so much seems a lot more reasonable. The over
function just lets us muddle around with whatever is inside our chosen sum type, should it be there to muddle around with. As such. Clear as mud, right?
Composition
Ok. So like all these examples, the one above is quite simple so that you can see what’s going on, but like all abstractions, it can leave us thinking “thanks pal, but this all seems a little bit much like hard work, surely”. But what makes it all worthwhile is when we combine a Prism
with other optics to make a giant super-optic. Remember our example from part one?
appData :: AppConfig
= AppConfig {
appData = Right 100
count = "Hello"
, title = DbConfig {
, dbConfig = "127.0.0.1"
ipAddress = 8080
, thePort
} }
What if we wanted to change what’s inside count
? That would be nice, wouldn’t it.
First, let’s make a prism
for count
.
countIntPrism :: Prism' (Either Error Int) Int
= prism' Right (\e -> case e of
countIntPrism Right b -> Just b
-> Nothing) _
…and a lens
for getting a count
out of the main appConfig
…
countLens :: Lens' AppConfig (Either String Int)
= lens count (\app newVal -> app { count = newVal } ) countLens
…and then we can compose them together to make a new thing.
fullCountInt :: Traversal' AppConfig Int
= countLens . countIntPrism fullCountInt
Let’s use it to grab that count
value…
initialCount :: Maybe Int
= preview fullCountInt appData
initialCount -- initialCount == Just 100
Great job!
And we can use it to change things too! With set
!
changedCount :: appConfig
= set fullCountInt 1000 appData
changedCount -- changedCount = AppConfig
-- { value = Right 1000
-- , title = "Hello!!!"
-- , dbConfig = DbConfig
-- { ipAddress = "127.0.0.1"
-- , thePort = 8080
-- }
-- }
And over
!
incrementedCount :: appConfig
= set fullCountInt (+1) appData
incrementedCount -- incrementedCount = AppConfig
-- { value = Right 101
-- , title = "Hello!!!"
-- , dbConfig = DbConfig
-- { ipAddress = "127.0.0.1"
-- , thePort = 8080
-- }
-- }
All our friends are here!
You might notice the new type Traversal
that has been produced by combining a Lens
and a Prism
. That’s an interesting thing in itself, but we’ll come to that another time.
That’s quite enough.
Make sense? If not, why not get in touch?
Further reading: