JakubArnold

Lens Tutorial - Introduction (part 1)

Jul 14, 2014

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 for Lens s a and enable the RankNTypes 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 fmapped.

λ> 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.

Want to hear about my upcoming book,
Haskell by Example?

Haskell by Example Book

Subscribe to receive updates and free content from the book. You'll also get a discount when the final version of the book is released.