Defining a monoid instance for a record type
Asked Answered
W

3

5

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?

Went answered 27/3, 2018 at 15:55 Comment(5)
Do you want to have mappend = undefined in your real Monoid instance, or will it join somehow two Options values?Acquirement
You could do this, but it makes me wonder whether we will save much effort with this. The idea of definining a Monoid is to define such mempty "for once and for all".Caraway
@Acquirement I left it undefined because it's not part of the question. I will, of course, eventually define it.Went
@WillemVanOnsem I can write Nothing 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 for Maybe a.Went
You can use Monoid instance for Maybe a, but it doesn't solve the problem you are asking about: mempty = Options mempty mempty mempty.Abshire
T
5

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 = ...
Thoron answered 27/3, 2018 at 16:16 Comment(3)
I think spelling the field names out directly actually makes it more likely that you’d miss a case here, since it will no longer give an error if there is a missing one.Morman
@DavidYoung -Wmissing-fields is on by default.Batchelor
Thanks for your answer. I went with just writing out all the fields, as I agree it's the most thoughtful way to do it. ("Explicit is better than implicit" or some such thing.)Went
A
3

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})}
Acquirement answered 27/3, 2018 at 16:17 Comment(0)
P
2

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
Pau answered 11/10, 2020 at 12:13 Comment(1)
I wrote about changing behaviour of fields without modifying the datatype (DerivingVia sums-of-products): deriving Monoid via GenericallySOP (PretendingVia Options '[ '[ First Integer, Maybe (Sum Integer), Maybe String ] ])Eugeniusz

© 2022 - 2024 — McMap. All rights reserved.