Reputation: 5285
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 Nothing
s when my Options
has a ton more fields?
Upvotes: 5
Views: 1320
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 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
Upvotes: 2
Reputation: 54971
I would recommend just writing the Nothing
s, 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 Monoid
s. 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
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 Integer
s 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