Last Updated: September 09, 2019
·
972
· jtobin

Eliminate boilerplate with RecordWildCards

I frequently wind up dealing with types like this:

import Control.Applicative

data AdminSettings = AdminSettings {
    adminFirstName      :: String
  , adminShirtSize      :: String
  , adminFavoriteDub    :: Double
  , adminUsualBreakfast :: String
  , adminFavoriteInt    :: Int
  }

Except often with many more records. You can define some real data by pattern matching on the constructor:

settings0 = AdminSettings "Jared" "M" 1.0 "Muesli" 1

But I think this is bad practice for ADTs with a large number of records. It ensures fragile code; if I add an 'adminPreferredPizza' record to the type, I have to ensure that I adjust my pattern matching so that everything is matched in the correct order.

The same goes for using applicative style in a case like this:

settings1 :: Maybe AdminSettings
settings1 = AdminSettings
            <$> pure "Jared"
            <*> pure "M"
            <*> pure 1.0
            <*> pure "Muesli"
            <*> Nothing

Order is not intrinsic to my data structure, so there's no need to do assignment via explicit order-sensitive traversals like this.

Straightforward record assignment is pretty declarative here:

settings2 :: AdminSettings
settings2 = AdminSettings { 
    adminFirstName      = "Jared"
  , adminShirtSize      = "M"
  , adminFavoriteDub    = 1.0
  , adminUsualBreakfast = "Muesli"
  , adminFavoriteInt    = 1
}

and isn't fragile when it comes to ordering:

settings3 :: AdminSettings
settings3 = AdminSettings { 
    adminFirstName      = "Jared"
  , adminUsualBreakfast = "Muesli"
  , adminShirtSize      = "M"
  , adminFavoriteInt    = 1
  , adminFavoriteDub    = 1.0
}

When you want to do something like the applicative example above, though, monadic style can get kind of verbose:

settings4 :: Maybe AdminSettings
settings4 = do
  firstName      <- return "Jared"
  shirtSize      <- return "M"
  favoriteDub    <- return 1.0
  usualBreakfast <- return "Muesli"
  favoriteInt    <- Nothing
  return $ AdminSettings { 
      adminFirstName      = firstName
    , adminShirtSize      = shirtSize
    , adminFavoriteDub    = favoriteDub
    , adminUsualBreakfast = usualBreakfast
    , adminFavoriteInt    = favoriteInt
  }

The solution is to use GHC's lesser-known and awesome RecordWildCards pragma:

{-# LANGUAGE RecordWildCards #-}

Now you can duplicate the above example without the tedious 'double binding' mumbojumbo:

settings5 :: Maybe AdminSettings
settings5 = do
  adminFirstName      <- return "Jared"
  adminShirtSize      <- return "M"
  adminFavoriteDub    <- return 1.0
  adminUsualBreakfast <- return "Muesli"
  adminFavoriteInt    <- Nothing
  return $ AdminSettings {..}

Which allows you to do something like 'order-independent traversals' on your record types.