Code Monkey home page Code Monkey logo

generic-data's Introduction

Generic data types in Haskell Hackage GitHub CI

Utilities for GHC.Generics.

Generic deriving for standard classes

Example: generically deriving Semigroup instances for products

Semi-automatic method using gmappend

data Foo a = Bar [a] [a] deriving Generic

instance Semigroup (Foo a) where
  (<>) = gmappend

This library also synergizes with the DerivingVia extension (introduced in GHC 8.6), thanks to the Generically newtype.

data Foo a = Bar [a] [a]
  deriving Generic
  deriving Semigroup via (Generically (Foo a))

These examples can be found in test/example.hs.


Note for completeness, the first example uses the following extensions and imports:

{-# LANGUAGE DeriveGeneric #-}

-- base
import Data.Semigroup (Semigroup(..))

-- generic-data
import Generic.Data (Generic, gmappend)
import Generic.Data.Orphans ()

The second example makes these additions on top:

{-# LANGUAGE
    DerivingStrategies,
    DerivingVia #-}  -- since GHC 8.6.1

-- In addition to the previous imports
import Generic.Data (Generically(..))

Supported classes

Supported classes that GHC currently can't derive: Semigroup, Monoid, Applicative, Alternative, Eq1, Ord1, Show1.

Other classes from base are also supported, even though GHC can already derive them:

  • Eq, Ord, Enum, Bounded, Show, Read (derivable by the standard);
  • Functor, Foldable, Traversable (derivable via extensions, DeriveFunctor, etc.).

To derive type classes outside of the standard library, it might be worth taking a look at one-liner.

Type metadata

Extract type names, constructor names, number and arities of constructors, etc..

Type surgery

generic-data offers simple operations (microsurgeries) on generic representations.

More surgeries can be found in generic-data-surgery, and suprisingly, in generic-lens and one-liner.

For more details, see also:

  • the module Generic.Data.Microsurgery;

  • the files test/lens-surgery.hs and one-liner-surgery.hs.

Surgery example

Derive an instance of Show generically for a record type, but as if it were not a record.

{-# LANGUAGE DeriveGeneric #-}
import Generic.Data (Generic, gshowsPrec)
import Generic.Data.Microsurgery (toData, derecordify)

-- An example record type
newtype T = T { unT :: Int } deriving Generic

-- Naively deriving Show would result in this being shown:
--
-- show (T 3) = "T {unT = 3}"
--
-- But instead, with a simple surgery, unrecordify, we can forget T was
-- declared as a record:
--
-- show (T 3) = "T 3"

instance Show T where
  showsPrec n = gshowsPrec n . derecordify . toData

-- This example can be found in test/microsurgery.hs

Alternatively, using DerivingVia:

{-# LANGUAGE DeriveGeneric, DerivingVia #-}
import Generic.Data (Generic)  -- Reexported from GHC.Generics

-- Constructors must be visible to use DerivingVia
import Generic.Data.Microsurgery (Surgery, Surgery'(..), Generically(..), Derecordify)

data V = V { v1 :: Int, v2 :: Int }
  deriving Generic
  deriving Show via (Surgery Derecordify V)

-- show (V {v1 = 3, v2 = 4}) = "V 3 4"

Related links

generic-data aims to subsume generic deriving features of the following packages:

  • semigroups: generic Semigroup, Monoid, but with a heavier dependency footprint.
  • transformers-compat: generic Eq1, Ord1, Show1.
  • generic-deriving: doesn't derive the classes in base (defines clones of these classes as a toy example); has Template Haskell code to derive Generic (not in generic-data).

Other relevant links.


Internal module policy

Modules under Generic.Data.Internal are not subject to any versioning policy. Breaking changes may apply to them at any time.

If something in those modules seems useful, please report it or create a pull request to export it from an external module.


All contributions are welcome. Open an issue or a pull request on Github!

generic-data's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

generic-data's Issues

gshowsPrec doesn't match `deriving Show` behavior in certain cases

While comparing generic-data's gshowsPrec to how deriving Show works, I noticed that there were a handful of cases where the two have differing behavior. I've distilled a small test suite to demonstrate these discrepancies:

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
module Main where

import Control.Monad
import Generic.Data
import GHC.Generics

data T1 = MkT1 { (##) :: () }
  deriving (Generic, Show)

data T2 = (:!:) () ()
  deriving (Generic, Show)

data T3 = MkT3 { unT3 :: () }
  deriving (Generic, Show)

testGshowsPrec :: (Generic a, GShow0 (Rep a), Show a)
               => Int -> a -> IO ()
testGshowsPrec p x = do
  let sp  = showsPrec p x ""
      gsp = gshowsPrec p x ""
  unless (sp == gsp) $ do
    putStrLn "Mismatch!"
    putStrLn $ "  showsPrec: "  ++ showsPrec p x ""
    putStrLn $ "  gshowsPrec: " ++ gshowsPrec p x ""

main :: IO ()
main = do
  testGshowsPrec  0 (MkT1 { (##) = () })
  testGshowsPrec  0 (() :!: ())
  testGshowsPrec 11 (MkT3 { unT3 = () })

When run, this produces:

Mismatch!
  showsPrec: MkT1 {(##) = ()}
  gshowsPrec: MkT1 {## = ()}
Mismatch!
  showsPrec: (:!:) () ()
  gshowsPrec: :!: () ()
Mismatch!
  showsPrec: (MkT3 {unT3 = ()})
  gshowsPrec: MkT3 {unT3 = ()}

Should Generically's Monoid instance be based on the Semigroup instance?

When a user has a custom Semigroup instance, they may want to derive a Monoid instance that uses their custom <>, which may behave differently from gmappend'!

Do you think it would it be a good idea if

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

So whenever someone derives both the Semigroup and Monoid instances via Generically all will be the same (except for less code generated due to reusing <>?).

But when only deriving Monoid via Generically, the derived instance would be consistent with the custom Semigroup instance (assuming that the gmempty fits the manual instance wrt the Monoid laws)

Optimize generic implementations of Read and Show

I don't think this is going to be a bottleneck for anyone, but the task could be an interesting exercise for someone to get some practice with how GHC rewrites Core.

  1. Set up some inspection tests for read and/or show comparing with GHC-derived instances (following some examples for other classes, currently in branch inspect)
  2. Optimize so the tests pass
  3. (optional) Do some benchmarks to get an idea of what we win

Generic Semigroup instance is not optimised

The following Semigroup instance seems to optimise poorly:

data Point2D a = Point2D !a !a
  deriving stock ( Show, Eq, Ord, Generic )

newtype Vector2D a = Vector2D ( Point2D a )
  deriving stock   ( Show, Generic )
  deriving newtype ( Eq, Ord )
  deriving Semigroup
    via GenericProduct ( Point2D ( Sum a ) )

I noticed this from the profiling output of an application which uses the above instance, which contained the following lines:

COST CENTRE   MODULE                        SRC                                                %time %alloc
gmappend      Generic.Data.Internal.Prelude src\Generic\Data\Internal\Prelude.hs:56:1-42         9.9    5.6
from'         Generic.Data.Internal.Utils   src\Generic\Data\Internal\Utils.hs:50:1-12           4.8    0.0

In this application, the Semigroup ( Vector2D a ) was the only instance using gmappend; moreover, all the occurrences of from' are associated with this single instance.
Switching to a hand-written instance made the other functions in the profiling report take up about 10% more of the total time, which corresponds to a significant speed up from writing mappend manually.

Note that the same issue occurs if we use the newtype Generically instead of GenericProduct (with the necessary adjustments to the code to allow the instance to be derived).

I've attached a quick (failing) inspection test using inspection-testing.

gmonoid-inspection.zip

Forgive me if it isn't expected that the generics in this instance should be optimised away; please let me know if there are better ways to go about this. Thanks.

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

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?

Surgery with derivingVia

We can derive many things using derivingVia and this package.

Is it possible to derive surgically altered things with deriving via? I could not get it compiling..

i.e. instead of

data T a = T { unT :: a }

instance (Show a) => Show (T a) where
  showsPrec n = gshowsPrec n . derecordify . toData

something like

data T a = T { unT :: a }
  deriving Show via Derecordify T a

latter yields the error

error:
    • Couldn't match representation of type ‘T a’
                               with that of ‘Derecordify T a’
        arising from the coercion of the method ‘showsPrec’
          from type ‘Int -> Derecordify T a -> ShowS’
            to type ‘Int -> T a -> ShowS’
    • When deriving the instance for (Show (T a))
    |
xxx |   deriving Show via Derecordify T a
    |            ^^^^

Is it just a missing instance Coercible (Derecordify a) a and Coercible a (Derecordify a)? Or is this harder to fix?

Better simplfiication for big types

The generic machinery is sadly not getting simplified away for big enough types. For an example, see the test case eqBigR =/= eqBigG in test/inspection.hs. The generically derived (==) (eqBigG) simplifies to a join point applied to a Rep in each of the 5 branches after pattern-matching on the first argument. How to get this join point to be inlined?

Products have Problematic Strictness

I tried using gap for the product explained in your paper Composing bidirectional programs monadically but the generic function has strictness in the parameters which caused the biparser drop bp = try bp *> drop bp <!> pure () to infinitely loop.

This is also problematic in base Data.Functor.Product.Product and I have detailed the problem more thoroughly in

haskell/core-libraries-committee#268

or

https://gitlab.haskell.org/ghc/ghc/-/issues/24898

This is probably a problem in more than just gap because I found with Product it to be a problem with

  • Applicative
  • Alternative
  • MonadPlus
  • MonadZip
  • Semigroup - impossible to create infinite data structures

so it would be worth checking those generic functions as well.

Is this package following PvP or SemVer versioning-policy?

As the current Vension is only in stackage-nightly, but stackage-lts is stuck on version 0.3.0.0 i wonder if this package follows SemVer or PvP.

https://github.com/commercialhaskell/stackage/blob/master/MAINTAINERS.md#lts-package-guarantees-and-exceptions

tl;dr:
In PvP x.y.z -> x.(y+1).0 is a major-release and not updated in lts, as these have always breaking changes
In SemVer x.y.z -> x.(y+1).0 is a minor-release with no breaking changes and x.y.z -> (x+1).0.0 are the major-releases. Thus the first is upgraded in stackage-lts.

Stackage defaults to PvP until the maintainer explicitly states that the package follows SemVer via issue on stackage-github.

I would like to see the current version in lts-13.xx instead of waiting for lts-14.xx (wich may be the new ghc-8.8 if we get no other ghc-8.6.5-point-release).

i see 2 ways to achieve that with the current state:

  1. Create issue on stackage to be registered as SemVer
  2. Do a 0.3.1.0, 0.4.1.0 & 0.5.1.0 with the current version and follow PvP after 0.6.0.0

However any decision is your call :)

Strange internal type errors with Coercible

(A really brief note about the issue, to be expanded later.)

For some reason these two lines are different (they shouldn't have to be)

, Coercible (Arborify l_t x) (Rep t x) ) -- Coercible is... (contd.)

, Coercible (Rep t x) (Arborify l_t x) ) -- ... not symmetric?

And the compiler doesn't like when I try to put them in a type synonym (RmvConstr, InsConstr or ConstrSurgery).

Even in the current state some tests fail to type check on GHC 8.0 and I cannot explain why:

-- Mysterious type error on 8.0
#if __GLASGOW_HASKELL__ >= 802
, testCase "removeConstr" $
"[Right A,Left 0,Right (C 1 2 3 4 5)]" @?=
(show . fmap (bimap unI (unit . toData) . removeConstr @"B" . toOR))
[A, B 0, x]
, testCase "insertConstr" $
"B 0" @?= (show . fromOR @T . insertConstr @"B" . Left) (I 0)
#endif

/home/sam/code/haskell/gdata/test/surgery.hs:66:49: error:
    • Couldn't match representation of type ‘Int’
                               with that of ‘M1
                                               C
                                               ('MetaCons "B" 'PrefixI 'False)
                                               (M1
                                                  S
                                                  ('MetaSel
                                                     'Nothing
                                                     'NoSourceUnpackedness
                                                     'NoSourceStrictness
                                                     'DecidedLazy)
                                                  (K1 GHC.Generics.R Int))
                                               ()’
        arising from a use of ‘removeConstr’

These are coercible: a manually written specialization of coerce at those types type checks.

-- both ways
> coerce (coerce (3 :: Int) :: M1 C ('MetaCons "B" 'PrefixI 'False) (M1 S ('MetaSel 'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (K1 R Int)) x) :: Int
3

Instances for the lattices package

It would be useful to implement derivations for PartialOrd, Lattice, BoundedJoinSemiLattice, etc from the lattices package.

Do you think that this should this fit in generic-data? Or rather it should go in the lattices package? Perhaps lattices is the right place for it as it is the more changing package (its interfaces recently changed)

cc @phadej

GHC 8.0 has problems with constructor surgeries

No idea what the problem with those two blocks is. These look like compiler bugs. For now they're disabled on GHC 8.0.

1. Type error

-- Type error on 8.0
#if __GLASGOW_HASKELL__ >= 802
, testCase "Constr-rmv-ins" $
rt A (fromOR . insertConstrT @"A" . removeConstrT @"A" . toOR)
#endif

Error message (it makes no sense, Linearize should not ever be applied to U1):

/home/sam/code/haskell/gdata/test/surgery.hs:58:22: error:
    • Couldn't match type ‘U1’
                     with ‘M1
                             C
                             ('MetaCons
                                (Generic.Data.Internal.Meta.MetaConsName
                                   (Generic.Data.Internal.Meta.MetaOf
                                      (Generic.Data.Internal.Surgery.Linearize U1)))
                                'PrefixI
                                'False)
                             U1’
        arising from a use of ‘insertConstrT’
    • In the first argument of ‘(.)’, namely ‘insertConstrT @"A"’
      In the second argument of ‘(.)’, namely
        ‘insertConstrT @"A" . removeConstrT @"A" . toOR’
      In the second argument of ‘rt’, namely
        ‘(fromOR . insertConstrT @"A" . removeConstrT @"A" . toOR)’

2. Infinite loop at compile time

-- Loops on 8.0
#if __GLASGOW_HASKELL__ >= 802
-- N.B. Identity (for constructor B) is inferred.
, testCase "removeConstr" $
"[Right A,Left (Identity 0),Right (C 1 2 3 4 5)]" @?=
(show . fmap (second (unit . fromOR') . removeConstrT @"B" . toOR))
[A, B 0, C 1 2 3 4 5]
, testCase "insertConstr" $
"B 0" @?= (show . fromOR @T . insertConstrT @"B" . Left) (Identity 0)
#endif

Build failure with GHC 9.4

Build is broken with GHC 9.4.1:

[14 of 19] Compiling Generic.Data.Internal.Generically ( src/Generic/Data/Internal/Generically.hs, /Users/abel/bin/src/generic-data/dist-newstyle/build/x86_64-osx/ghc-9.4.1/generic-data-0.9.2.1/build/Generic/Data/Internal/Generically.o, /Users/abel/bin/src/generic-data/dist-newstyle/build/x86_64-osx/ghc-9.4.1/generic-data-0.9.2.1/build/Generic/Data/Internal/Generically.dyn_o )

src/Generic/Data/Internal/Generically.hs:73:32: error:
    Ambiguous occurrence ‘Generically’
    It could refer to
       either ‘GHC.Generics.Generically’,
              imported from ‘GHC.Generics’ at src/Generic/Data/Internal/Generically.hs:24:1-19
           or ‘Generic.Data.Internal.Generically.Generically’,
              defined at src/Generic/Data/Internal/Generically.hs:71:1
   |
73 | instance Generic a => Generic (Generically a) where
   |                                ^^^^^^^^^^^

and many more of these...

Incorrect instance of GShowSingle for (f :.: g)

The left parameter (f) in f :.: g is not a generic representation, but a plain representation. Only the right parameter is a Generic representation. See example here.

The instance

instance (GShowSingle Identity f, GShowSingle p g)
  => GShowSingle p (f :.: g) where
  ...

should instead be

instance (Show1 f, GShowSingle p g)
  => GShowSingle p (f :.: g) where
  ...

similarly to the instance for Rec1 f.


A simple example of this would be trying to derive Show1 for a type equivalent to Compose.

newtype MyCompose f g a = MyCompose (f (g a))
  deriving Generic1
  deriving Show1 via Generically1 (MyCompose f g)

gives the error

    * Could not deduce (Generic.Data.Internal.Show.GShowSingle
                          Data.Functor.Identity.Identity f)
        arising from the 'deriving' clause of a data type 
declaration
      from the context: Show1 g

Build failure with GHC 9.6 (base-4.18)

Building generic-data-1.0.0.0 with GHC 9.6.1 alpha2 produces this error and few similar ones:

src/Generic/Data/Internal/Generically.hs:250:10: error: [GHC-43085]
    • Overlapping instances for Eq (Generically1 f a)
        arising from a use of ‘ghc-prim-0.10.0:GHC.Classes.$dm/=’
      Matching instances:
        instance forall k (f :: k -> *) (a :: k).
                 (Generic1 f, Eq (Rep1 f a)) =>
                 Eq (Generically1 f a)
          -- Defined in ‘GHC.Generics’
        instance (Generic1 f, Eq1 (Rep1 f), Eq a) => Eq (Generically1 f a)
          -- Defined at src/Generic/Data/Internal/Generically.hs:250:10
    • In the expression:
        ghc-prim-0.10.0:GHC.Classes.$dm/= @(Generically1 f a)
      In an equation for ‘/=’:
          (/=) = ghc-prim-0.10.0:GHC.Classes.$dm/= @(Generically1 f a)
      In the instance declaration for ‘Eq (Generically1 f a)’
    |
250 | instance (Generic1 f, Eq1 (Rep1 f), Eq a) => Eq (Generically1 f a) where
    |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

How to derive instances for HKD style records

Hello,

I'm trying to modify test/record.hs example to derive Semigroup/Monoid instances for HKD style records (https://reasonablypolymorphic.com/blog/higher-kinded-data/).

type family HKD f a where
  HKD Identity a = a
  HKD f a = f a

data MyRecord f = MyRecord
  { _field1 :: HKD f Int
  , _field2 :: HKD f Bool
  } deriving Generic

Semigroup/Monoid instance methods get errors like

  Occurs check: cannot construct the infinite type: f ~ Alt f
     arising from a use of coerce

I don't have enough knowledge to understand these errors. Is it possible to use generic-data library to derive those instances with such change? If so, how to do it.

Thank you very much for any advice.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.