generic-data-surgery icon indicating copy to clipboard operation
generic-data-surgery copied to clipboard

Add full constructor removal example

Open treeowl opened this issue 3 years ago • 3 comments

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?

treeowl avatar Nov 11 '21 23:11 treeowl

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

Lysxia avatar Nov 13 '21 17:11 Lysxia

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.

treeowl avatar Nov 15 '21 17:11 treeowl

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.

Lysxia avatar Nov 15 '21 18:11 Lysxia