Skip to content

Modifying Monoid (Generically a)'s mconcat. #413

Description

@L0neGamer

TLDR: Monoid (Generically a) does not derive its methods from a's Semigroup instance, so produces incongruous methods when used to derive instances

background: #324

Introduction

The Monoid instance for Generically a can result in an incorrect implementation if the Semigroup instance wasn't also Generically derived.

Here is the current implementation of Monoid (Generically a):

-- | @since base-4.17.0.0
instance (Generic a, Monoid (Rep a ())) => Monoid (Generically a) where
  mempty :: Generically a
  mempty = Generically (to (mempty :: Rep a ()))

  mappend :: Generically a -> Generically a -> Generically a
  mappend = (<>)

Here is an example where it goes awry:

newtype L a = L [a]
  deriving (Generic, Show, Eq)
  deriving Monoid via (Generically (L a))

instance Semigroup (L a) where
  L [] <> l = l
  l <> _ = l
> L [1] <> L [2]
L [1]
> L [1] `mappend` L [2]
L [1,2]

The important thing to note is that as per my Monad of No Return proposal, we will be removing mappend as a method instance, so this particular case will be solved.

However, the same problem crops up for mconcat:

> mconcat [L [1], L [2]]
L [1,2]

Proposed Change

This proposal suggests that we change the definition of Monoid (Generically a) to

instance (Generic a, Monoid (Rep a ()), Semigroup a) => Monoid (Generically a) where
  mempty :: Generically a
  mempty = Generically (to (mempty :: Rep a ()))

  mconcat :: [Generically a] -> Generically a
  mconcat = foldr (coerce @(a -> a -> a) (<>)) mempty
  {-# INLINE mconcat #-}

This results in:

> mconcat [L [1], L [2]]
L [1]

This means that Generically a's Monoid instance may be unlawful with respect to its Semigroup instance, but since the main use of this newtype is deriving, we want the most correct implementation for that purpose. To further this line of thought, as part of this proposal I plan to elaborate on these risks in documentation and recommend that if users do derive Monoid using Generically, they should be deriving Semigroup as well for consistency and lawfulness.

The proposer does not know if this is the best definition, and welcomes discussion. Coercions and inline pragma were used in the hopes that that helps performance, but if it seems unlikely to do so we can go with some simpler definition.

mappend is already being removed as a class method (eventually), so mconcat is the only method to fix.

Alternatives

It may be recommended to add memptyGeneric and sappendGeneric as methods for more granular defining of Monoid and Semigroup using Generic.

Using sconcat would be nice but has different strictness properties to most mconcats.

We could decide that deriving Monoid without deriving semigroup in the same way is unsupported behaviour, point that out in the documentation and possibly even wire in a warning for it.

Impact

I have had a brief rg of my local hackage download, and I cannot see any places where Monoid is defined and Semigroup isn't, which would result in someone relying on the behaviour outlined; further, I can't see any obvious uses of Generically in values, meaning it is unlikely someone is using Generically to get around having to define their own monoid and semigroup instances.

This may also mean that this adjustment is unnecessary.

Implementation

The current implementation can be found in the GHC gitlab here: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/16011

Addendum

If reviewers wish to play around themselves:

-- example.hs
-- load with ghci, and comment/uncomment lines `(1)` and `(2)`, run `main`
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE UndecidableInstances #-}

import GHC.Generics
import Data.Coerce
import Data.Semigroup
import Data.List.NonEmpty qualified as NE

main :: IO ()
main = do
  let l1 = L [1]
      l2 = L [2]
  print (l1 <> l2)
  print (l1 `mappend` l2)

  print (foldr (<>) mempty [l1, l2])
  print (mconcat [l1, l2])

newtype L a = L [a]
  deriving (Generic, Show, Eq)
  deriving Monoid via (Generically (L a)) -- (1)
  -- deriving Monoid via (Generically' (L a)) -- (2)

instance Semigroup (L a) where
  L [] <> l = l
  l <> _ = l

newtype Generically' a = Generically' a

instance (Generic a, Semigroup (Rep a ())) => Semigroup (Generically' a) where
  (<>) :: Generically' a -> Generically' a -> Generically' a
  Generically' a <> Generically' b = Generically' (to (from a <> from b :: Rep a ()))

instance (Generic a, Monoid (Rep a ()), Semigroup a) => Monoid (Generically' a) where
  mempty :: Generically' a
  mempty = Generically' (to (mempty :: Rep a ()))

  mconcat :: [Generically' a] -> Generically' a
  mconcat = foldr (coerce @(a -> a -> a) (<>)) mempty
  {-# INLINE mconcat #-}

Metadata

Metadata

Assignees

No one assigned

    Labels

    approvedApproved by CLC voteawaits-mergeApproved, but MR is still unmergedbase-4.24Implemented in base-4.24 (GHC 10.2)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions