Haskell: escaping the record update syntax hell

Posted on July 11, 2012

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:

  1. Take an object and a fuction as parameters;
  2. Apply function to the selected field value;
  3. 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.