user4601931
user4601931

Reputation: 5285

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?

Upvotes: 5

Views: 1320

Answers (3)

dopamane
dopamane

Reputation: 1315

Check out the generic-monoid package on hackage. Specifically, the Data.Monoid.Generic module. We can automatically derive the semigroup and monoid instances with the DerivingVia extension. That way you can avoid having to write extensive mappend and mempty functions when your records are large and every field in the record is already a monoid. The documentation gives the following example:

data X = X [Int] String
  deriving (Generic, Show, Eq)
  deriving Semigroup via GenericSemigroup X
  deriving Monoid    via GenericMonoid X

This works because [Int] is a monoid and String is a monoid. In both fields mappend is concatenation and mempty is the empty list [] and empty string "".Therefore we can make X a monoid.

X [] "" == (mempty :: X)
True

Keep in mind, Haskell requires that you need a semigroup if you want to define a Monoid. We see that the typeclass of Monoid has the Semigroup constraint:

class Semigroup a => Monoid a where
 ...

Unfortunately not all fields are monoids in your Option record. Specifically, Maybe Int does not satisfy the Semigroup constraint out-of-the-box because Haskell doesn't know how you want to mappend two Ints, perhaps you would add (+) them or maybe you'd like to multiply (*) them etc. We can fix this easily by borrowing common monoids from Data.Monoid (or writing our own) and making all the fields of Option monoids.

{-# DeriveGeneric #-}
{-# DerivingVia   #-}

import GHC.Generics
import Data.Monoid
import Data.Monoid.Generic

data Options = Options
  { _optionOne   :: First Integer
  , _optionTwo   :: Sum   Integer
  , _optionThree :: Maybe String
  } 
  deriving (Generic, Show, Eq)
  deriving Semigroup via GenericSemigroup Options
  deriving Monoid    via GenericMonoid Options

You left the mappend function undefined in the question so I just picked some monoids at random to show variety (you may find the Maybe wrappers interesting because their mempty is Nothing). First's mappend always picks the first argument over the second and its mempty is Nothing. Sum's mappend just adds Integers and its mempty is zero 0. Maybe String is already a monoid with mappend as String concatenation and mempty as Nothing. Once each field is a monoid, we can derive the semigroup and monoid via GenericSemigroup and GenericMonoid.

mempty :: Options
Options {
  _optionOne = First { getFirst = Nothing },
  _optionTwo = Sum { getSum = 0 },
  _optionThree = Nothing
}

Indeed, mempty matches our expectation and we didn't have to write any monoid or semigroup instances for our Options type. Haskell was able to derive it for us!

P.S. A quick note about using Maybe a as a monoid. Its mempty is Nothing, but it also requires a to be a semigroup. If either argument to mappend (or since we're talking about semigroups its <>) is Nothing, then the other argument is chosen. However, if both arguments are Just, we use a's underlying semigroup instance's <>.

instance Semigroup a => Semigroup (Maybe a) where
    Nothing <> b       = b
    a       <> Nothing = a
    Just a  <> Just b  = Just (a <> b)

instance Semigroup a => Monoid (Maybe a) where
    mempty = Nothing

Upvotes: 2

Jon Purdy
Jon Purdy

Reputation: 54971

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 = ...

Upvotes: 5

0xd34df00d
0xd34df00d

Reputation: 1505

If the Monoid instance for your record type follows naturally from the Monoid instances of the record fields, then you could use Generics.Deriving.Monoid. The code could would look like this:

{-# LANGUAGE DeriveGeneric #-}

import GHC.Generics
import Generics.Deriving.Monoid

data Options = { .. your options .. }
             deriving (Show, Generic)

instance Monoid Options where
  mempty = memptydefault
  mappend = mappenddefault

Note that the record fields have to be Monoid too, so you will have to wrap your Integers into Sum or Product (or possibly some other newtype) depending on the exact behavior you want.

Then, assuming you want the resulting monoid to be synced with addition on top of Integer and use the Sum newtype, the resulting behavior would be:

> mempty :: Options
Options {_optionOne = Nothing, _optionTwo = Nothing, _optionThree = Nothing}
> Options (Just $ Sum 1) (Just $ Sum 2) (Just $ Sum 3) <> Options (Just $ Sum 1) (Just $ Sum 2) Nothing
Options {_optionOne = Just (Sum {getSum = 2}), _optionTwo = Just (Sum {getSum = 4}), _optionThree = Just (Sum {getSum = 3})}

Upvotes: 3

Related Questions