Lens Tutorial - SimpleLens

2017-07-12
haskelllens

If you have been using Haskell for a while or browsed the packages in Hackage, there is a good chance that you have come across a package called lens. lens provides a large assorment of types and functions that simplify data access and updates in a functional way. It can help us solve many problems, but the size and scope of the package, as well as complex type signatures, make it challenging for new users to approach. Moreover, we may not need all of the tools from the lens package. I believe the best way to start using Lenses is Haskell is by implementing a simple subset.

The first tool we want to discuss are Lenses. Lenses are functional references. Reference means that they point to parts of a value and allow us to access or modify them. Functional means that they provide composibility. Lenses abstract getters and setters for Haskell product types (records).

Motivation

First, we will review record update syntax in Haskell. You can follow along by starting up ghci. We define a simple data type, make a value of that type and update one of the rows.

That is relatively simple, but it becomes more complex when we introduce an embedded record type.

Lenses can help simplify this problem.

Background

There are two types from base that we need to understand before we begin with Lenses: Identity and Const. We will focus on how they work. Once we get to Lenses we understand their purpose.

Data.Functor.Identity

You may have come across the id function before. It takes a value and returns it. id is useful for when you are required to provide a function but do not want to change the value.

Identity is similar id, but it is a newtype that has one type parameter and has an instance of Functor. Identity is a container type like Maybe.

We can make the Functor instance for Identity a bit clearer.

Try out Identity in ghci.

Data.Functor.Const

const is another common function. It takes two items, returns the first and discards the second.

Much like the relation between id and Identity, there are const and Const. Const has two type parameters a and b, but it only takes and returns a value of type a. b is a phantom type. b does not exist on the right side of the declaration and we do not provide a value of type b.

The Functor instance for Const is also interesting. It ignores the function and does not apply it to value of type a. The returned valued remains constant.

We can try out Const in ghci.

SimpleLens

We will implment a simplified version of Lens called SimpleLens.

RankNTypes implies ExplicitForAll. It allows us to use forall in a type alias. We import Const and Identity as we discussed above.

A SimpleLens has two polymorphic types. s is a container type like Maybe, [], (,), Either, etc. a is the type in the container that we want to reference. For example, SimpleLens Maybe Int, SimpleLens (,) String, SimpleLens Either Int, etc.

On the right hand side there is a type class restriction for f. We will need another container type f that has a Functor instance. This is where Const and Identity will be used.

The first argument is (a -> f a), this a function that takes an a and returns a in the f container, which has a Functor instance. a is the type we are referencing that is contained by s, then we pass it an instance of s and we get s contained in f.

Now we define our first lens. Person will be the s type and String for name and Int for age will be the a types in each SimpleLens.

_name is a lens that focuses on the name record of Person. a_to_f_a is the function we need to pass in (a -> f a) and we apply it directly to the name record of Person. _age is a lens that focuses on the age record of Person.

By themselves we cannot do anything directly with _name or _age. We will need some helper functions. Before continuing, here is what we should now about SimpleLens so far:

  • SimpleLens is a type synonym.

  • SimpleLens has two polymorphic type parameters: s is a container type, a the type of a value contained in s.

  • It takes (a -> f a) and s and it returns f s. The container type s wrapped in a second container f.

  • f has a Functor instance.

SimpleLens helper functions

view

The first helper function we will implement is view. view takes a SimpleLens and an s then it returns an a from s. view functions as a getter. It does not change the value we are referencing, it just returns it. We will use Const to retrieve a from s.

We can use view and _name together to get the name record from Person.

The way view and _name work together may still be a bit unclear. It is useful to write out what view name would look like.

If you remember the definition of Functor Const the f function will not applied to the value inside Const. (\ppName -> Person ppName pAge) will be ignored and getConst $ Const pName will be reduced to pName.

To solidify our understanding of view and SimpleLens, we will repeat the same for age.

set

set is the setter helper function for SimpleLens. It takes a SimpleLens, an a that we want to insert in s, s and it returns s with the new a value. set uses Identity to apply the new a into s and return s.

Here is an example.

Just like we did above, we explicit write out set with _name to make sure we understand how set works with a lens.

over

over is the same as set except instead of taking a value of a, it takes a function (a -> a). It allows us to modify an existing value inside s

And a simple example.

Expanded form of over _age.

SimpleLens with embedded record

Here is clean implementation of SimpleLens.hs that you can use to play around with.

References