generic-data-surgery
generic-data-surgery copied to clipboard
Add full constructor removal example
For hairy reasons, I want to redefine the Plutus BuiltinByteString type thus:
data BuiltinByteString
= BuiltinByteString {-# UNPACK #-} ByteString
| NeverBS_ !Void
Of course, any Generic-derived instances are likely to do unfortunate things with the NeverBS_ constructor. Is there a nice way to give this type a Generic instance that looks like this?
instance Generic BuiltinByteString where
type Rep BuiltinByteString = M1 _ (M1 _ (M1 _ (Rec0 ByteString)))
to (M1 (M1 (M1 (K1 bs)))) = BuiltinByteString bs
from (BuiltinByteString bs) = M1 (M1 (M1 (K1 bs)))
I would imagine the thing to do is write a type data FakeBIB = FakeBIB {-# UNPACK #-} ByteString deriving Generic in the same module and then adjust its Rep, to, and from as needed. I know how to do this manually with custom type families, but is there a nice way?
One big hurdle for this use case is that surgeries need a Generic instance to start from, so at best the modified instance can only be given to a separate newtype. Once you get past that, you can construct the surgery RemoveConstr "NeverBS_" Void to define the new Rep, together with the value-level surgery functions removeConstr and insertConstr for from and to.
{-# LANGUAGE DataKinds, DeriveGeneric, TypeFamilies, UndecidableInstances #-}
module D where
import Data.Void
import Data.Functor.Identity
import GHC.Generics
import Generic.Data.Surgery
data BB = BB {-# UNPACK #-} !Bool | NeverBB !Void
deriving Generic
newtype BB' = BB' { unBB' :: BB }
instance Generic BB' where
type Rep BB' = Operate (Rep BB) (RemoveConstr "NeverBB" Void)
from = from . fromOR' . unRight . removeConstr @"NeverBB" @1 @(Identity Void) . toOR . unBB' where
unRight :: Either (Identity Void) b -> b
unRight (Left (Identity v)) = absurd v
unRight (Right x) = x
to = BB' . fromOR . insertConstr @"NeverBB" @1 @(Identity Void) . Right . toOR' . to
The technique I've ended up using is to write, in the same module,
data FakeBuiltinByteString
= FakeBuiltinByteString {-# UNPACK #-} ByteString
deriving Generic
Then I can write a type family
type family TwiddleBS fake where
TwiddleBS (D1 ('MetaData _type_name mod_name pkg_name newtypeness)
(C1 ('MetaCons _con_name x y)
z)) =
D1 ('MetaData "BuiltinByteString" mod_name pkg_name newtypeness)
(C1 ('MetaCons "BuiltinByteString" x y)
z)
This just goes into the generic representation of FakeBuiltinByteString and changes the names, leaving everything else alone (including, most importantly, the package identifier and the DecidedStrictness). I wrote the to and from functions by hand, because this particular case is so simple there's no point doing anything else.
Can we generalize? The first step, and the easier one, is to generalize TwiddleBS. One option would look like
type Twiddle :: TyconName -> [(DataconName, [FieldName])] -> (k -> Type) -> k -> Type
Another option would be to take a defunctionalization symbol explaining how to unmunge the names (e.g., strip two underscores from the end).
Constructing to and from would have to be left primarily to the user.
Ah yes, transforming just the types without changing the runtime representation is indeed much easier, and there is actually a module for that in generic-data (not -surgery). There is already such a "microsurgery" for renaming constructors, and I would accept adding one for renaming types and changing metadata more generally.