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 #-}
TLDR:
Monoid (Generically a)does not derive its methods froma's Semigroup instance, so produces incongruous methods when used to derive instancesbackground: #324
Introduction
The
Monoidinstance forGenerically acan result in an incorrect implementation if theSemigroupinstance wasn't alsoGenericallyderived.Here is the current implementation of
Monoid (Generically a):Here is an example where it goes awry:
The important thing to note is that as per my Monad of No Return proposal, we will be removing
mappendas a method instance, so this particular case will be solved.However, the same problem crops up for
mconcat:Proposed Change
This proposal suggests that we change the definition of
Monoid (Generically a)toThis results in:
This means that
Generically a'sMonoidinstance may be unlawful with respect to itsSemigroupinstance, 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 deriveMonoidusingGenerically, they should be derivingSemigroupas 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.
mappendis already being removed as a class method (eventually), so mconcat is the only method to fix.Alternatives
It may be recommended to add
memptyGenericandsappendGenericas methods for more granular defining of Monoid and Semigroup usingGeneric.Using
sconcatwould be nice but has different strictness properties to mostmconcats.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
rgof 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: