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 Int
s, 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 Integer
s 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
mappend = undefined
in your realMonoid
instance, or will it join somehow twoOptions
values? – AcquirementMonoid
is to define suchmempty
"for once and for all". – Carawayundefined
because it's not part of the question. I will, of course, eventually define it. – WentNothing
fifteen times, for sure. I was just wondering if there was a slick way to populate a record type with all the same value. Or maybe a way to use the monoid instance forMaybe a
. – WentMonoid
instance forMaybe a
, but it doesn't solve the problem you are asking about:mempty = Options mempty mempty mempty
. – Abshire