Real, Intermediate Haskell Lenses - Part 1
Recently, I started reading swarm
, a resource gathering + programming game built with Haskell. I went to swarm
’s very first commit. The 200-liner is able to render the following,
and I was already stumped by the code! What in the lens are these snippets —
-- r :: Robot,
-- robotProgram :: Lens' Robot [Command]
-- p :: Command
(r & robotProgram .~ p)
-- world :: Lens' GameState [[Char]]
-- row, col :: Int
preuse $ world . ix row . ix col
-- inventory :: Lens' GameState (Map Item Int)
inventory . at (Resource h) . non 0 += 1
What’re the functions the fancy symbols like .~
? And what do combinators such as preuse
and ix
do? Hopefully you can understand these after this piece. Kmett’s lenses library is focused around the Lens
type, so I first give you a whirlwind tour of Lens
(part 1). But to understand the snippet, we need to explore a more general Traversal
type, and, how Lens
/Traversal
interact with State
(mtl
-ified with MonadState
). That will be part 2.
A Tour of Lens
A Lens
is basically type synonym for
type Lens s a = forall f. Functor f => (a -> f a) -> s -> f s
Function-wise, a Lens
can be thought of as both a getter and a setter. We can transform a Lens s a
both into a getter and a setter by applying the Lens
with the right functor. s
is the larger object that should contain some a
.
get :: Lens s a -> (s -> a)
get l s = getConst $ l Const s
over :: Lens s a -> (a -> a) -> (s -> s)
over l f a = runIdentity $ l (\b -> Identity (f b)) a
I explicitly wrapped the (s -> a)
in get
to emphasize the high-order form. I.e., get
is a transformation from Lens s a
to a (s -> a)
. A getter gives you an a
from a s
. In get
’s definition, f
is specialized to Const
to make everything work.
A setter typically has the form s -> a -> s
. I.e., set
takes in object s
and a constant a
to update some inner field with, and gives you back the updated s
. Instead of set
, we naturally get a more general over
here, allowing us to consider the previous a
in the update.
The following crucial point will wrap up the background: A Lens s a
contains the concrete logic to “hit” an a
from a s
. For example,
data Person = Person { age :: Int, address :: Address }
data Address = Address { streetName :: String }
-- Contains the logic to hit the address from a person
ex1 :: Lens' Person String
ex1 f s = (\streetName' -> s { address = (address s) { streetName = streetName'} }) <$> f ((streetName . address ) s)
-- Contains the logic to hit a Char from a [Char]. The only way to have a valid definition is if
-- we choose a specific index
ex2 :: Lens' [Char] Char
ex2 f s = (\a' -> setAt 3 a' s) <$> f (s !! 3)
Note, we can also make nonsensical Lens
, for example
ex3 :: Lens' String Int
ex3 f s = s <$ f 0
ex4 :: Lens' [Char] Char
ex4 f s = s <$ f 'a'
ex1 :: Lens' String Int
should provide a means to get/set an Int
in a String
, but that’s absurd — String
doesn’t contain an Int
. However, Lens'
allow us to write such a vacuous Lens' String Int
. We provide a concrete Int
(0
in this case) in order to lift it into f Int
via f 0
. Once we are in the functor, we apply fmap (const s) :: Int -> String
to build a f String
(simplified with <$
). In fact, we can vacuously write a value of type Lens' s Int
.
What about ex2
? It should be possible to (unsafely) get/set a Char
inside of a [Char]
. However, our implementation does not update our s :: [Char]
at all. We are again providing a concrete value ('a' :: Char
in this case) just to get into f Char
, which s <$
latches onto.
For all concrete s
and a
, Lens' s a
can be written vacuously. It is up to you to provide a reasonable definition. After applying its 3 arguments, a sensible Lens' s a
should update some a
within the given s
.