Search code examples
haskelltypeclassmonoids

Defining a monoid instance for a record type


Suppose I have a type like

data Options = Options
  { _optionOne :: Maybe Integer
  , _optionTwo :: Maybe Integer
  , _optionThree :: Maybe String
  } deriving Show

with many more fields. I would like to define a Monoid instance for this type, for which the mempty value is an Options with all fields Nothing. Is there a more concise way to write this than

instance Monoid Options where
  mempty = Options Nothing Nothing Nothing
  mappend = undefined

which would avoid the need to write a bunch of Nothings when my Options has a ton more fields?


Solution

  • I would recommend just writing the Nothings, or even spelling out all the record fields explicitly, so you can be sure you don’t miss a case when adding new fields with a different mempty value, or reordering fields:

    mempty = Options
      { _optionOne = Nothing
      , _optionTwo = Nothing
      , _optionThree = Nothing
      }
    

    I haven’t tried it before, but it seems you can use the generic-deriving package for this purpose, as long as all the fields of your record are Monoids. You would add the following language pragma and imports:

    {-# LANGUAGE DeriveGeneric #-}
    import GHC.Generics (Generic)
    import Generics.Deriving.Monoid
    

    Add deriving (Generic) to your data type and wrap all your non-Monoid fields in a type from Data.Monoid with the combining behaviour you want, such as First, Last, Sum, or Product:

    data Options = Options
      { _optionOne :: Last Integer
      , _optionTwo :: Last Integer
      , _optionThree :: Maybe String
      } deriving (Generic, Show)
    

    Examples:

    • Last (Just 2) <> Last (Just 3) = Last {getLast = Just 3}
    • First (Just 2) <> First (Just 3) = First {getFirst = Just 2}
    • Sum 2 <> Sum 3 = Sum {getSum = 5}
    • Product 2 <> Product 3 = Product {getProduct = 6}

    Then use the following function(s) from Generics.Deriving.Monoid to make your default instance:

    memptydefault :: (Generic a, Monoid' (Rep a)) => a
    mappenddefault :: (Generic a, Monoid' (Rep a)) => a -> a -> a
    

    In context:

    instance Monoid Options where
      mempty = memptydefault
      mappend = ...