Lens Tutorial - SimpleLens

July 12, 2017
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.

λ> data User = User { name :: String, email :: String } deriving (Show)
λ> user = User "Sanjay" "owner@sanjay.com"
λ> updatedUser = user { email = "admin@sanjay.com" }

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

λ> data Phone = Phone { phoneNumber :: String } deriving (Show)
λ> data Employee = Employee { name :: String , phone :: Phone } deriving (Show)
λ> employee = Employee "Guillermo" (Phone "52-33-3333-7400")
λ> updatePhone = (phone employee) { phoneNumber = "52-33-3333-1111" }
λ> updateEmployee = employee { phone = updatePhone }

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.

λ> id "Hello world!"
"Hello world!"
λ> id (1 + 1)
2

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.

newtype Identity a = Identity { runIdentity :: a }

instance Functor Identity where
  fmap = coerce

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

instance Functor Identity where
  fmap f (Identity i) = Identity $ f i

Try out Identity in ghci.

λ> (++ " world!") <$> Identity "Hello"
Identity "Hello world!"
λ> runIdentity $ (+1) <$> Identity 1
2

Data.Functor.Const

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

λ> const True "Hello world!"
True
λ> const 1 2
1
λ> const "Hello world!" Nothing
"Hello world!"

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.

newtype Const a b = Const { getConst :: a }

instance Functor (Const m) where
  fmap _ (Const v) = Const v

We can try out Const in ghci.

λ> not <$> Const True
Const True
λ> getConst $ (+1) <$> Const 1
1
λ> (++ " world!") <$> Const "Hello"
Const "Hello"

SimpleLens

We will implment a simplified version of Lens called SimpleLens.

{-# LANGUAGE RankNTypes #-}

import Data.Functor.Const
import Data.Functor.Identity

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

type SimpleLens s a = forall f. Functor f => (a -> f a) -> s -> f s

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.

data Person =
  Person
    { name :: String
    , age  :: Int
    } deriving (Eq,Read,Show)

-- expanded type signature
-- _name :: forall f. Functor f => (String -> f String) -> Person -> f Person
_name :: SimpleLens Person String
_name a_to_f_a (Person pName pAge) = (\ppName -> Person ppName pAge) <$> a_to_f_a pName

-- expanded type signature
-- _age :: forall f. Functor f => (Int -> f Int) -> Person -> f Person
_age :: SimpleLens Person Int
_age a_to_f_a (Person pName pAge) = (\ppAge -> Person pName ppAge) <$> a_to_f_a pAge

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

-- view :: ((a -> f a) -> s -> f s) -> s -> a
view :: SimpleLens s a -> s -> a
view l = getConst . l Const

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

λ> view _name $ Person "Marina" 21
"Marina"

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.

-- view _name
view_name :: Person -> String
view_name (Person pName pAge) = getConst $ (\ppName -> Person ppName pAge) <$> Const pName

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.

-- view _age
view_age :: Person -> Int
view_age (Person pName pAge) = getConst $ (\ppAge -> Person pName pAge) <$> Const pAge

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.

-- set :: ((a -> f a) -> s -> f s) -> a -> s -> s
set :: SimpleLens s a -> a -> s -> s
set l b = runIdentity . l (\_ -> Identity b)

Here is an example.

λ> set _name "Serena" $ Person "Marina" 21
Person "Serena" 21

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

-- set _name
set_name :: String -> Person -> Person
set_name b (Person pName pAge) = runIdentity $ (\ppName -> Person ppName pAge) <$> Identity b

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

-- over :: ((a -> f a) -> s -> f s) -> (a -> a) -> s -> s
over :: SimpleLens s a -> (a -> a) -> s -> s
over l f = runIdentity . l (Identity . f)

And a simple example.

λ> over _age (+1) $ Person "Marina" 21
Person "Marina" 22

Expanded form of over _age.

-- over _age
over_age :: (Int -> Int) -> Person -> Person
over_age a_to_a (Person pName pAge) = runIdentity $ (\ppAge -> Person pName ppAge) <$> Identity (a_to_a pAge)

SimpleLens with embedded record

data Phone = 
  Phone 
    { phoneNumber :: String 
    } deriving (Show)

data Employee = 
  Employee 
    { employeeName :: String
    , employeePhone :: Phone 
    } deriving (Show)
    
_phoneNumber :: SimpleLens Phone String
_phoneNumber a_to_f_a (Phone phoneNum) = (\pPhoneNum -> Phone pPhoneNum) <$> a_to_f_a phoneNum

_employeePhone :: SimpleLens Employee Phone
_employeePhone a_to_f_a (Employee eName ePhone) = (\eEPhone -> Employee eName eEPhone) <$> a_to_f_a ePhone

_employeeName :: SimpleLens Employee String
_employeeName a_to_f_a (Employee eName ePhone) = (\eEName -> Employee eEName ePhone) <$> a_to_f_a eName

main :: IO ()
main = do 
  let matthias         = Employee "Matthias" $ Phone "123-345-8888"
      matthiasNewPhone = set (_employeePhone . _phoneNumber) "222-333-1212" matthias
      matthiasJr       = set (_employeePhone . _phoneNumber) "432-234-1177" $ over _employeeName (++ " Jr.") $ matthias
  print matthias
  print matthiasNewPhone
  print matthiasJr

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

References