Lysxia / generic-data

Generic data types in Haskell, utilities for GHC.Generics
https://hackage.haskell.org/package/generic-data
MIT License
44 stars 9 forks source link

Deriving Monoid now incurs a Semigroup instance that might not be present #25

Closed sheaf closed 4 years ago

sheaf commented 4 years ago

The following code works with generic-data-0.6.0.1 but not generic-data-0.7.0.0 due to #18:

data Point2D a = Point2D !a !a
  deriving Generic
newtype Vector2D a = Vector2D { tip :: Point2D a }
  deriving ( Semigroup, Monoid )
    via Generically ( Point2D ( Sum a ) )
* Could not deduce (Semigroup (Point2D (Sum a)))
    arising from the 'deriving' clause of a data type declaration

The changes in #18 mean that, when attempting to derive the Monoid ( Vector2D a ) instance, GHC goes looking for a Semigroup ( Point2D ( Sum a ) ) instance as opposed to using the Semigroup ( Vector2D a ) instance that has just been generically derived.

I understand that #18 enables generically deriving a Monoid instance while re-using the underlying Semigroup instance, but in my opinion it is more common to want to derive both instances at the same time.

What's the equivalent way of writing the above example starting from version 0.7.0.0 of the library?

Lysxia commented 4 years ago

Oops, I didn't think about that use case. It doesn't seem possible to get the old behavior, so instead we have to write the Monoid instance by hand, and we even have to coerce it manually :(

instance Monoid a => Monoid (Vector2D a) where
  mempty = Vector2D gmempty
  -- also, on base < 4.11:
  -- mappend = (<>)

I could add another newtype that does this, so we could write via SemigroupGenerically (Point2D (Sum a)) (modulo choosing a better name). (In hindsight, #18 should have been another newtype if I had anticipated this.)

One technicality is whether to define mappend for that newtype as gmappend' or (<>). It doesn't matter if all fields ensure that mappend = (<>) syntactically; not even performance should be affected.

sheaf commented 4 years ago

See also the readme for my acts library where I have

data Point2D a = Point2D !a !a
  deriving stock ( Show, Generic )
  deriving ( Act ( Vector2D a ), Torsor ( Vector2D a ) )
    via Vector2D a
newtype Vector2D a = Vector2D { tip :: Point2D a }
  deriving stock Show
  deriving ( Semigroup, Monoid, Group )
    via Generically ( Point2D ( Sum a ) )

I find it quite convenient to use a single newtype to derive the three different stuctures, and in my opinion it's a nice usability upside that this library has over generic-monoid.

Anyway it shouldn't matter too much, as even if Generically keeps the current behaviour we can also provide a newtype which provides the old behaviour. Although I would prefer it to have a less specific name than SemigroupGenerically, as I find deriving Monoid via SemigroupGenerically a to be a bit weird.

Lysxia commented 4 years ago

So it looks like we will have to have two versions of Generically, and backwards compatibility dictates that we don't touch the current Generically (even though it is frustrating because the new behavior seems useful more often).

Arguably, a catch-all Generically has limited applicability, because there is more than one implementation for most classes, such as Monoid here (although the issue with this one really stems from a historical accident). So it is natural that we should be looking at different newtypes with more specialized meanings.

The main purpose here is to derive all three of Semigroup, Monoid and Group from the same product structure, so I have the following name suggestions for the new newtype (in order of my preference):

  1. GenericProduct
  2. ProductType
  3. AsProduct