dreixel / generic-deriving

BSD 3-Clause "New" or "Revised" License
44 stars 32 forks source link

Recursive deriving #84

Closed treeowl closed 2 years ago

treeowl commented 2 years ago

Sometimes, whole sets of types are missing Generic and other instances, and it's desirable to write orphan instances to work around that. For example, there might be a module

module Sigh where

data Gel = ...
data Gummy = Goofer !Int Gel

To derive many instances for Gummy generically, an app might need to derive instances for both types:

deriving instance Generic Gel
deriving instance Generic Gummy
instance ToJSON Gel
instance ToJSON Gummy

Unfortunately, this gets much nastier when not all the types or constructors are exported:

module Sigh (Gummy, worm) where
  ...

Is there any way to write functions to help in this kind of situation? What about when there are mutually recursive types involved?

RyanGlScott commented 2 years ago

You may be interested in th-reify-many's reifyManyWithoutInstances function, which recursively names mentioned in data type declarations that do not have instances of a particular class. For your program, an example of its usage might look like:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module Foo where

import "aeson" Data.Aeson (ToJSON)
import "aeson" Data.Aeson.TH (deriveToJSON, defaultOptions)
import "base" GHC.Generics (Generic)
import "generic-deriving" Generics.Deriving.TH (deriveAll0)
import "th-reify-many" Language.Haskell.TH.ReifyMany
import Sigh (Gummy)

$(do -- Don't bother trying to define a Generic instance for Int
     let genPred n = n /= ''Int

     genericInsts <- reifyManyWithoutInstances ''Generic [''Gummy] genPred
                       >>= traverse deriveAll0
     toJSONInsts  <- reifyManyWithoutInstances ''ToJSON [''Gummy] (const True)
                       >>= traverse (deriveToJSON defaultOptions)
     pure $ concat $ genericInsts ++ toJSONInsts)

Template Haskell reification works on non-exported entities, so this works even if Gel is not exported. Moreover, reifyManyWithoutInstances remembers names it has already seen before, so this also works if Gel and Gummy are mutually recursive.

treeowl commented 2 years ago

That sounds like exactly what I'm looking for! Thanks!

treeowl commented 2 years ago

By the way, why doesn't Int have a Generic instance? That seems very strange.

RyanGlScott commented 2 years ago

Before GHC 8.0, Int actually did have a Generic instance, which looked something like:

-- Int
data D_Int
data C_Int

instance Datatype D_Int where
  datatypeName _ = "Int"
  moduleName   _ = "GHC.Int"
  packageName  _ = "base"

instance Constructor C_Int where
  conName _ = "" -- JPM: I'm not sure this is the right implementation...

instance Generic Int where
  type Rep Int = D1 D_Int (C1 C_Int (S1 NoSelector (Rec0 Int)))
  from x = M1 (M1 (M1 (K1 x)))
  to (M1 (M1 (M1 (K1 x)))) = x

This Rep instance was dodgy, as it claimed that the structure of Int was something like data Int = Int Int. Back then, GHC.Generics had no support for unlifted types like Int# at all, so this was the best one could do. Ultimately, this instance (and similar instances for primitive types) were removed for the reasons outlined here.

Nowadays, one could hypothetically define a Generic instance for Int by using URec to encode the fact that its field is Int#. Then again, the amount of primitive types you could make this work for is pretty limited, so I've never seriously thought about trying to resurrect these kinds of Generic instances. (To be honest, I think GHC.Generics' design vis-à-vis unlifted types is flawed as a whole, but that's a story for another day...)