Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 393
user4601931 Avatar asked Mar 27 '18 15:03

user4601931


People also ask

What are monoids Haskell?

From HaskellWiki. In Haskell, the Monoid typeclass (not to be confused with Monad) is a class for types which have a single most natural operation for combining values, together with a value which doesn't do anything when you combine it with others (this is called the identity element).

Why use monoids Haskell?

One answer is that thinking in terms of monoids can be very beneficial to parallelism and efficient data structures. Using the Monoid interface in Haskell allows you to leverage the many convenient functions that work with them.

Is list a Monoid?

Strings, lists, and sequences are essentially the same monoid.

What is a semigroup Haskell?

From HaskellWiki. The Semigroup represents a set with an associative binary operation. This makes a semigroup a superset of monoids. Semigoups have no other restrictions, and are a very general typeclass.


2 Answers

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 = ...
like image 148
Jon Purdy Avatar answered Sep 28 '22 17:09

Jon Purdy


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})}
like image 23
0xd34df00d Avatar answered Sep 28 '22 17:09

0xd34df00d