The problem
Suppose you have a data type Sample
:
data Sample = Sample {
someNum :: Int
, someText :: String
, someFoo :: Foo
} deriving (Eq, Show)
data Foo = Foo {
fooBool :: Bool
, fooPair :: (Int, Int)
} deriving (Eq, Show)
..and you have a value of this type, s
:
let s = Sample 1 "Sample" $ Foo True (2, 3)
Having s
, you need to change something inside it’s Foo
. How to do it? The most obvious is to write something like:
s2 = s { someFoo = foo { fooBool = False } }
where foo = someFoo s
Looks crappy, but it is okay. And what if we need to increment s
’s someFoo
’s fooPair
? The issue is that increment depends on the previous value, so we need to write something like
s3 = s2 { someFoo = foo { fooPair = newPair } }
where foo = someFoo s2
newPair = (p1 + 1, p2 + 1)
(p1, p2) = fooPair foo
Wow! It looks completely scary. Imagine how it would like if we had a three or four nesting levels!
The idea
We can make things easier with a simple helper functions:
modFoo :: Sample -> (Foo -> Foo) -> Sample
modFoo s fcn = s { someFoo = fcn $ someFoo s }
modFooPair :: Foo -> ((Int, Int) -> (Int, Int)) -> Foo
modFooPair f fcn = f { fooPair = fcn $ fooPair f }
Using these functions, we can define s3
as:
s3 = modFoo s2 $ \f -> modFooPair f $ \(a, b) -> (a + 1, b + 1)
It looks definitely better! But now we find that both modFoo
and modFooPair
functions follow the same pattern:
- Take an object and a fuction as parameters;
- Apply function to the selected field value;
- Return a new object based on a passed one with the selected field set to the of the function’s return value.
It is boring to write such boilerplate code for each data field by hand. Cann’t it be automated?
The solution
Yes, it can. With the Template Haskell extension, we can inspect the data type definitions and to generate the code we want.
This approach lies at the heart of the Data.Mutators package. For each field of the each record syntax constructor of the given data type, Data.Mutators will generate the set-
and mod-
functions. For example, given a data type
data ObjectType = ObjectType {
something :: SomethingType
}
after invoking genMutators ''ObjectType
we will get the following functions
setSomething :: ObjectType -> SomethingType -> ObjectType
modSomething :: ObjectType -> (SomethingType -> SomethingType) -> ObjectType
Obviously, the set-
function sets the field value. The mod-
function applies a function to the field value – it is quite handful when we need to modify a field using its existing value.
The names of the generated functions are build by upper-casing the field name’s first character and prefixing it with “set” or “mod”. This behavior can be adjusted with an alternate function, genMutators'
, which takes an additional argument of type String -> (String, String)
. This function should return a tuple containing names for setter and modifier functions (exactly in this order), using the field name passed to it.
Enjoy!