This article is the first in the upcoming series that aims to explain the
Haskell’s lens
library and the
ideas behind it in an approachable way. Don’t worry if you’re new to Haskell,
the only prerequisites here should be understanding of the Functor
type
class, and understanding how records and algebraic data types work in Haskell.
We won’t be using the lens
library in this article yet. The API we’ll develop
will be exactly the same, but for the sake of learning I’ll try to show you how
everything works and why it works by re-implementing it from scratch.
Keep in mind that lenses are a very advanced topic in Haskell and it takes some time to truly understand them. Don’t worry if you don’t understand everything at first read.
The motivation behind lenses
If you’re coming from an imperative language like Ruby or Java, you’re probably used to seeing code like this:
project.owner.name = "John"
The OOP people would call this a violation of the Law of Demeter, but let’s ignore that it’s a bad practice for now. The question here is, can we achieve something similar in Haskell?
data User = User { name :: String, age :: Int } deriving Show
data Project = Project { owner :: User } deriving Show
setOwnerName :: String -> Project -> Project
setOwnerName newName p = p { owner = (owner p) { name = newName } }
Now we can already see how this is less than ideal. In order to change the name
of the owner
, we need to re-assign the owner field in the Project
with the
new User
, which is updated using the record syntax. We could do this in
multiple steps as follows.
Code blocks with λ>
denote GHCi session.
λ> let bob = User { name = "Bob", age = 30 }
λ> let project = Project { owner = bob }
λ> let alice = bob { name = "Alice" }
λ> let project2 = project { owner = alice }
This is very tedious compared to the original Ruby example, especially since we need to keep re-building the original structure as we go deeper and deeper.
A naive lens implementation
This is where lenses come to help you out. In essence, lenses are just getters and setters which you can compose together. In a naive approach the type might look something like the following:
data NaiveLens s a = NaiveLens
{ view :: s -> a
, set :: a -> s -> s }
Following the convention of the official lens library
I’ve named the type parameters s
and a
, where s
is the object and a
is the focus. In our example above the s
would be Project
and a
would
be a String
, since we’re trying to change the name of the project’s user.
Now given a lens of type NaiveLens User String
we can easily change the
name of a user
λ> let john = User { name = "John", age = 30 }
λ> set nameLens "Bob" john
User {name = "Bob", age = 30}
How is such lens implemented? It’s simply a getter and a setter.
nameLens :: NaiveLens User String
nameLens = NaiveLens name (\a s -> s { name = a })
The problem with this approach of sticking a getter and a setter into a data
type is that it doesn’t scale very well. If we wanted to do something like
increment the value at the target by one, we would have to first view
the
current value, apply +1 to it, and then set
the new value. We could
encapsulate this by providing the lens with a third function call over
:
over :: (a -> a) -> s -> s
We could use this similarly to set
.
λ> let john = User { name = "John", age = 30 }
λ> over ageLens (+1) john
User {name = "John", age = 31}
ageLens :: NaiveLens User Int
ageLens = NaiveLens age
(\a s -> s { age = a })
(\f s -> s { age = f (age s) })
The problem is that now we need to provide a getter and two setters for each lens, even if we just use one.
If you’ve been using Haskell for a while you’ve probably seen the magical
function const
. It’s actually not magical at all, it simply has a type of a -> b -> a
, which allows us to turn over :: (a -> a) -> s -> s
into set :: a -> s -> s
by partially applying it, which leads to the definition of set
as
follows.
set :: NaiveLens s a -> a -> s -> s
set ln a s = over ln (const a) s
Here’s how the whole code looks now
data NaiveLens s a = NaiveLens
{ view :: s -> a
, over :: (a -> a) -> s -> s }
set :: NaiveLens s a -> a -> s -> s
set ln a s = over ln (const a) s
Lenses with side effects and more
Now we can see that over
is definitely useful, but what if our modifier
function needs to perform some side effects? For example we might want to send
the current value over the network to determine the new value. We could go on
as before and add yet another function called overIO
, which would look as the
following:
overIO :: (a -> IO a) -> s -> IO s
But this means our simple pair of a getter and a setter has grown into a getter
and two setters again. Not to mention that we might want to use over
in more
settings than just IO
. Here’s how the type would look now.
data NaiveLens s a = NaiveLens
{ view :: s -> a
, over :: (a -> a) -> s -> s
, overIO :: (a -> IO a) -> s -> IO s }
This is the point where the magical generalization of what is called the van
Laarhoven lens comes into play. First step is that we can write our overIO
in a more general way by swapping IO
for a Functor
, which gives us the
following type.
overF :: Functor f => (a -> f a) -> s -> f s
For the sake of keeping this article short I’m going to tell you that overF
is everything we need in order to implement view
, set
, over
and overIO
.
Which means we no longer need a Lens
record type, since we’ll have just one
function.
type Lens s a = Functor f => (a -> f a) -> s -> f s
By making this a type alias instead of a newtype
or data
we get one amazing
property of lenses. You can define your own lenses without depending on the
lens
library. Any function which has the appropriate type signature is a
lens, there is no magic.
One thing to note here is that we do need to enable the
RankNTypes
extension for
this type alias to compile. To do that simply add the following snippet to the
first line of your file.
{-# LANGUAGE RankNTypes #-}
or if you’re following along in GHCi type :set -XRankNTypes
. I won’t be
explaining this in this article since it’s quite a complicated topic, but if
you’re interested in learning more, a simple google search will yield a lot of
good results.
Implementing over
, set
and view
in terms of Lens s a
Let’s summarize before we move on. We started with an idea that a lens
represents a getter and a setter into some data type. Then we generalized the
setter to work with functions (using over
). Last we realized that over
is
not good enough when we want to do side effects, so we moved to overIO
and
finally generalized it to the van Laarhoven lens of Functor f => (a -> f a) -> s -> f s
.
So far I’ve only told you that our new Lens s a
can behave like over
, set
and view
, but we need to prove it to really understand why. In order to do
this we’ll make use to two Functor
instances that come from the base
library, namely Data.Functor.Identity
and Control.Applicative.Const
. Let’s
start with the simplest one, that is implementing over
with the Identity
functor.
over
with Identity
First of all, here’s the implementation of Identity
.
newtype Identity a = Identity { runIdentity :: a }
instance Functor Identity where
fmap f (Identity a) = Identity (f a)
The reason why this is useful is because we can put a value in, let it behave as a functor, and then take the value out.
The final type of over
that we’re looking for is over :: Lens s a -> (a -> a) -> s -> s
. We can read that as: Given a lens focusing on an a
inside of
an s
, and a function from a
to a
, and an s
, I can give you back a
modified s
from applying the function to the focus point of the lens.
over :: Lens s a -> (a -> a) -> s -> s
over ln f s = _
If you’re on GHC 7.8.x you can copy the exact snippet above and get an error telling you what type is needed in place of
_
(this functionality is provided by so called type holes.) Also don’t forget that you need to add the type alias forLens s a
and enable theRankNTypes
extension as mentioned above.
We’ll inline the Lens
type synonym, just so that we can see what is really
going on. Don’t worry if the type looks scary, it will all make sense in a
short while.
over :: (Functor f => (a -> f a) -> (s -> f s)) ->
(a -> a) -> s -> s
over ln f s = _
I’ve added a few parentheses, especially around the s -> f s
, to make it
clear as we go along with partial applications. Keep in mind that Lens
is
just a function, nothing more.
We only have one function of the type a -> f a
available here to pass into
the lens ln
, and that is Identity
.
over :: Lens s a -> (a -> a) -> s -> s
over ln f s = _ (ln Identity)
Using GHCi to play with types
If you want to play along in GHCi, there’s a neat little trick you can do to
interactively play with types. Say that you want to see the type of ln Identity
λ> let ln = undefined :: (Functor f => (a -> f a) -> (s -> f s))
λ> :t ln Identity
:: s -> Identity s
The reason why this works is because the undefined
can take on any type.
Since we’re just trying to make the types align, you won’t get an error from
trying to evaluate the undefined
, you’ll just a type error. This way you can
keep trying to partially apply things to see if the types match as you expect.
Anyway, moving on. We haven’t really used our function f
yet, and there will
be no more a
to apply it to ones we give something to the lens ln
. This is
why we need to apply it before we stick in the Identity
, or compose it with
the Identity
to be specific.
over :: Lens s a -> (a -> a) -> s -> s
over ln f s = _ (ln (Identity . f))
Now our current type hole if (s -> f s) -> s
, which means we can stick in our
s
. To make this syntactically more pleasing we’ll replace some parentheses
with $
.
over :: Lens s a -> (a -> a) -> s -> s
over ln f s = _ $ ln (Identity . f) s
Hang in, we’re almost done. The last thing we need do, as our type hole tells
us, is f s -> s
, which means we basically need to rip off the functor. This
is easy to do as we’re using the Identity
functor, so we just apply
runIdentity
.
over :: Lens s a -> (a -> a) -> s -> s
over ln f s = runIdentity $ ln (Identity . f) s
If you’re feeling adventurous, we can rewrite this using point free style.
over :: Lens s a -> (a -> a) -> s -> s
over ln f = runIdentity . ln (Identity . f)
view
with Const
Now let’s move on to view
, where the type is simply view :: Lens s a -> s -> a
. We can read this as: Given a lens that focuses on an a
inside of an s
,
and an s
, I can give you an a
.
This part is probably the most magical, since the type of the Lens s a
is (a -> f a) -> s -> f s
and we’re trying to implement something that’s s -> a
,
which means we need to have a way to turn the final f s
into an a
. The key
to this is the Const
functor.
newtype Const a b = Const { getConst :: a }
instance Functor (Const a) where
fmap _ (Const a) = Const a
Let’s break this down into steps and first explain how Const
works. Const
is a wrapper which takes a value, hides it deep inside, and then pretends to be
a functor containing something else, which is why it ignores the function
you’re trying to fmap
over const. Here’s an example:
λ> :t Const "hello"
:: Const String b
We’ve hidden a "hello"
string inside a Const
, now let’s try to apply a
boolean function to it using fmap
.
λ> let boolBox = fmap (&& False) (Const "hello")
λ> :t boolBox
Const [Char] Bool
The Const
has taken over to be a type of Const String Bool
. If we fmap
over a function Bool -> Double
we’ll get a Const String Double
.
λ> :t fmap (\_ -> 1.2 :: Double) boolBox
:: Const String Double
The important thing to keep in mind here is that the Const
simply ignores the
function we’re fmapping and takes on the new type, while keeping our original
String
safe. We can extract it back at any time we want, no matter how many
things we’ve fmap
ped.
λ> getConst boolBox
"hello"
λ> getConst $ fmap (\_ -> 1.2 :: Double) boolBox
"hello"
The actual view
implementation
Let’s do this using type holes again.
view :: Lens s a -> s -> a
view ln s = _
We can approach this the same way as we did before when implementing over
using Identity
. First of all, here’s the type of Lens s a
again in case you
forgot Functor f => (a -> f a) -> s -> f s
.
If you squint hard enough you can see that if we somehow pass a function to
ln
, we’ll get back another function of the type s -> f s
, which we can give
our s
, and then the only thing remaining is to extract the resulting a
out
of the f s
. Again the only function that fits here is Const
.
view :: Lens s a -> s -> a
view ln s = _ $ ln Const
The type of the hole here is (s -> f s) -> a
, which means we can apply our
s
on the right side as we did with over
.
view :: Lens s a -> s -> a
view ln s = _ $ ln Const s
Now all we’re left with is f s -> a
, and because we know that the f s
is
actually Const a s
we can get back the a
using getConst
view :: Lens s a -> s -> a
view ln s = getConst $ ln Const s
And there you go, we got ourselves a view
. I won’t be showing how to
implement set
step by step, since it can be trivially defined either in terms
of over
, which is good enough for us.
set :: Lens s a -> a -> s -> s
set ln x = over ln (const x)
Writing our own lenses
In order to use lenses we actually need to have some lenses. As said earlier,
we do not need the lens
library to define a new lens, we only need a function
with the type of Functor f => (a -> f a) -> s -> f s
. Let’s make one!
We’ll start by implementing the _1
lens, which focuses on a first element of
a pair. The type will be Lens (a,b) a
or specifically Functor f => (a -> f a) -> (a,b) -> f (a,b)
, in another words Given a pair of (a,b)
the lens
focuses on the first element of the pair, which is a
.
_1 :: Functor f => (a -> f a) -> (a,b) -> f (a,b)
_1 f (x,y) = _
An interesting thing about pure functions in Haskell is that more often than not, there is only one way to implement a function so that it typechecks. We can use the types as we did earlier to guide us while implementing this.
Ok let’s get going. We have three values available (via the function
parameters), f :: a -> f a
, x :: a
and y :: b
. The only thing we can do
here is apply f
to x
.
_1 :: Functor f => (a -> f a) -> (a,b) -> f (a,b)
_1 f (x,y) = f x
This will fail to typecheck, since we’re trying to return f a
instead of f (a,b)
. What else can we do now? We know f
is a Functor
, which means we can
use fmap
. We also know that we need to somehow use y
to compose the result.
If you think about this for a while, all we can really do is fmap
some
function on the result of f x
_1 :: Functor f => (a -> f a) -> (a,b) -> f (a,b)
_1 f (x,y) = fmap _ (f x)
The result is that the type of _
in this case must be a -> (a, b)
. That’s
it, we only have one thing of type b
, which is y
, and the a
we can take
just form the parameter passed to the lambda, hence giving us the following.
_1 :: Functor f => (a -> f a) -> (a,b) -> f (a,b)
_1 f (x,y) = fmap (\a -> (a, y)) (f x)
Whoa, did we just write an actual lens? I believe we did sir. Let’s test things out!
Using lenses
Now that we got ourselves a view
and _1
lens, let’s play!
λ> view _1 (1,2)
1
We can also use set
and over
to change the value
λ> set _1 3 (1,2)
(3,2)
λ> over _1 (+3) (1,2)
(4,2)
Let’s see how to define a lens for the original User
and Project
types.
data User = User { name :: String, age :: Int } deriving Show
data Project = Project { owner :: User } deriving Show
We’ll start with a lens for the User
’s name
, which simply has the type
Lens User String
. There’s no magic here, we’ll just follow the same pattern
as we did with the _1
lens.
nameLens :: Lens User String
nameLens f user = fmap (\newName -> user { name = newName }) (f (name user))
As you can see this is just mechanical work. We can define the other two lenses
for age
and owner
by simply copy pasting the first one and changing a few
things around.
ageLens :: Lens User Int
ageLens f user = fmap (\newAge -> user { age = newAge }) (f (age user))
ownerLens :: Lens Project User
ownerLens f project = fmap (\newOwner -> project { owner = newOwner }) (f (owner project))
Composing lenses together
Because lenses are just functions (remember that Lens s a
is just a type
alias) we can compose them using the ordinary function composition .
ownerNameLens :: Lens Project String
ownerNameLens = ownerLens.nameLens
Let’s test this out:
λ> let john = User { name = "John", age = 30 }
λ> let p = Project { owner = john }
λ> view ownerNameLens p
"John"
λ> set ownerNameLens "Bob" p
Project {owner = User {name = "Bob", age = 30}}
Conclusion of part 1
Congratulations to you if you’ve read this far, you now have a good
understanding of how the basic Lens s a
works. This is not the end though,
since lenses are a very large subject and there is a lot of ground to cover.
The followup posts to this one will cover the more general Lens s t a b
type,
folds, traversals, prisms, isos, using template haskell to generate lenses, and
much more!
If you’re curious especially about the Lens s t a b
type and what it means,
it’s basically just a small generalization of what we’ve devleoped here.
Compare the following two:
type Lens' s a = Functor f => (a -> f a) -> s -> f s
type Lens s t a b = Functor f => (a -> f b) -> s -> f t
This might look weird at first, but it’s not if you apply it to a specific data type, such as:
Lens (Int, String) (Double, String) Int Double
(Int -> f Double) -> (Int, String) -> f (Double, String)
It simply allows you to change the type of the underlying structure, but as I said earlier, we’ll cover this more in one of the upcoming blog posts.