One of my favourite aspects of functional programming is the emphasis on immutability - rather than mutating things and making direct changes to them, you work with copies and the differences are reflected in the construction. I won’t harp on about the benefits of doing this, but instead I’ll note that it isn’t all ponies and unicorns. Working with purely functional data can be difficult, especially when you have nested data structures; you often have to clone something deep inside and then reassemble everything by hand - yuck!
Edward Kmett
’s lens
package aims to solve this problem. lens
provides “families of lenses, isomorphisms, folds, traversals, getters and setters”. This is probably still a little unclear, and it’s easiest to see what lens
gives you from an example:
data Point = Point
_y :: Double } deriving (Show)
{ _x,
data Monster = Monster
_monsterLocation :: Point
{deriving (Show)
}
'Point
makeLenses ''Monster
makeLenses '
= Monster (Point 0 0) ogre
I’ve got 2 data structures for my little game - a 2D Point
, and a Monster
. I’ve made an example Monster
- the ogre
. All Monster
s have a location, and we presumably want them to move around. To move the ogre
without lens
, it might look like:
> ogre { _monsterLocation = (_monsterLocation ogre) {
λ= _x (_monsterLocation ogre) + 1
_x
} }Monster {_monsterLocation = Point {_x = 1.0, _y = 0.0}}
URGH! All of that, just to move 1 to the right?! Lets see how this looks with lens
:
> monsterLocation.x +~ 1 $ ogre
λMonster {_monsterLocation = Point {_x = 1.0, _y = 0.0}}
That’s much better! We’ve composed the monsterLocation
and x
lens to move into the x
part of the Point
of a Monster
, and then used +~ 1
to add one to it. Almost magically, these changes gets applied and our new Monster
is rebuild and returned to us.
lens
considers itself to come with “batteries included”, but this seems like it’s selling itself short - it really gives you a whole power station! As of this time of writing, lens has 99 operators and covers a huge range of operations you might wish to do on data. A recent addition is “prisms”, which are 0-or-1 target traversals, meaning they could give you either 1 answer or no answer at all, depending on the data that is viewed through them.
Natural numbers are a good example of this:
nat :: SimplePrism Integer Natural
= prism toInteger $ \ i ->
nat if i < 0
then Left i
else Right (fromInteger i)
Now we can ask if an Int
is a Natural
, by trying to view an Int
through the nat
prism:
> 5 ^? nat
λJust 5
> (-5) ^? nat
λNothing
Prisms fit in perfectly with the lens API, which means that we can compose them like any other lens - just like we did with Monster
s and Point
s above. In this example, we’ve got a pair of Int
s, perhaps from user input. We want to multiple each side of this input by 2, but only if it’s already a natural number. It sounds tricky, and that we’d likely need conditionals to pull it off, but not so with lens
!
> both.nat *~ 2 $ (-3,4)
λ-3,8)
(
> both.nat *~ 2 $ (8,4)
λ16,8) (
When lens
views the (-3)
through the nat
prism, it doesn’t view anything, because (-3)
is of course, not a natural number. As such, we just return (-3)
, unchanged. We’ve composed the nat
prism with both
to view “both sides” of a tuple through the nat
prism. This example is a variant of the official prism documentation, so be sure to check that out if you like this!
As always, I’ve only covered a tiny portion of the full API. lens
can also do traversals, monadic actions, folds, along with including a bunch of lens for common data structures such as Set
, Text
, Vector
and much more.
You can contact me via email at ollie@ocharles.org.uk or tweet to me @acid2. I share almost all of my work at GitHub. This post is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.