Overloading record fields with lens
After seeing this comment on reddit asking if
lens
solves the “overloading issue” with Haskell’s
record systems, I thought I’d write up what I found here. By the way,
The answer is yes, it can solve the problem with
makeFields
. The code for this blog post is available
as a github gist.
Often, when making lenses I’ll use makeLenses
. This
is one of the simplest Template Haskell lens functions, and tries
to make a named lens for each field in your record.
If you try to avoid the clashing field names by doing something
like this:
data Foo = Foo { _oneFoo :: Double, _twoFoo :: Double }
data Bar = Bar { _oneBar :: Double, _twoBar :: Double }
'Foo
makeLenses ''Bar makeLenses '
… you’ll end up with many different (and long) names for everything. That’s not nice.
Instead you can use makeFields
. This will give you a
typeclass for each field called (for example) HasOne.
If we’d used makeFields in our example above, both Foo and Bar would
be instances of HasOne, we could refer to the fields using the name
one
, and everything would be just rosy. A more complete example is
below. I’ve used 2D and 3D vectors (Vec2
and Vec3
) to demonstrate
how we can create lenses named x
, y
, and z
super-simply just
using makeFields
.
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE FlexibleInstances #-}
module Fields where
import Control.Lens
import Control.Lens.TH
data Vec2 = Vec2
_vec2X :: Double
{ _vec2Y :: Double
,deriving (Eq, Read, Show)
}
data Vec3 = Vec3
_vec3X :: Double
{ _vec3Y :: Double
, _vec3Z :: Double
,deriving (Eq, Read, Show)
}
'Vec2
makeFields ''Vec3
makeFields '-- each _vecNC field can now be referred to as 'x', 'y', or 'z'
-- results in: Vec2 2 2
= over x (+1) $ Vec2 1 2
example1 -- results in: Vec3 1 2 300
= over z (*100) $ Vec3 1 2 3 example2
Here, makeFields
uses some default rules to strip away the underscore
and record type name to transform our field names
(_vec3X
, vec2X
) into the lens name (x
).
These rules can be customised by using
makeFieldsWith
.
We can use ghci to examine the created typeclasses (using :t HasX
at
the GHCI prompt):
class HasX c_a24V e_a24W | c_a24V -> e_a24W where
x :: Functor f => (e0 -> f e0) -> c0 -> f c0
-- Defined at Fields.hs:21:1
instance HasX Vec2 Double -- Defined at Fields.hs:21:1
instance HasX Vec3 Double -- Defined at Fields.hs:22:1
Aside from the strange type variable names, this is fairly simple:
the HasX
class has a single value, x
, which is the Lens into our
record. The type parameters to HasX are the record type, then
the field type. That means we can write a normal Vec2 like this.
v :: (v ~ Vec2, HasX v Double) => v
= Vec2 10 10 v
Note that the type could be written simply as v :: Vec2
1.
Our more complicated type signature is
just intended to illustrate how the type parameters get ‘filled in’.
The tilde (~
) in a type signature denotes equality.
A more useful example, in which we want to write a function that zeroes out the x component of any vector, is below:
-- | Take any type of vector with an x component and zero it
zeroX :: HasX t Double => t -> t
= x .~ 0
zeroX
-- We get a Vec2 0 10
zeroed2 :: Vec2
= zeroX $ Vec2 100 10 zeroed2
We can also make 2- and 3- tuples instances of HasX and HasY like this:
instance HasX (Double, a) Double where
= _1
x
instance HasY (a, Double) Double where
= _2
y
instance HasX (Double, a, b) Double where
= _1
x
instance HasY (a, Double, b) Double where
= _2
y
instance HasZ (a, b, Double) Double where
= _3 z
The Control.Lens.TH module is full of useful functions like
makeFields
, so I definitely recommend reading through the
documentation to see if there’s anything useful to you in there.
This isn’t strictly true, because we’re also asserting that Vec2 must be an instance of HasX v Double. If we removed the
makeFields
call, this line would complain that there was no instance, whereas if we’d written the type simply asv :: Vec2
, it would compile.↩︎