Lens Tutorial - SimpleLens
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)
2Identity 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 = coerceWe can make the Functor instance for
Identity a bit clearer.
instance Functor Identity where
fmap f (Identity i) = Identity $ f iTry out Identity in ghci.
λ> (++ " world!") <$> Identity "Hello"
Identity "Hello world!"
λ> runIdentity $ (+1) <$> Identity 1
2Data.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 vWe 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.IdentityRankNTypes 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 sA 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:
SimpleLensis a type synonym.SimpleLenshas two polymorphic type parameters:sis a container type,athe type of a value contained ins.It takes
(a -> f a)andsand it returnsf s. The container typeswrapped in a second containerf.fhas aFunctorinstance.
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 ConstWe 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 pNameIf 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 pAgeset
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" 21Just 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 bover
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" 22Expanded 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 matthiasJrHere is clean implementation of SimpleLens.hs that you can use to play around with.