4

Is there something like Show (deriving Show) that only uses an algebraic datatype's constructors? (please don't mind that I'm using the word constructor, I don't know the right name...)

The reason for this question is that with many of my algebraic datatypes I don't want to bother with making their contents also derive Show, but I still want to gain some debug information about the constructor used without having to implement showing every constructor...

An alternative could be a function that gives me the constructors name, that I can use in my own implementation of show.

This of course needs to do some compiler magic (auto deriving) because the whole idea behind is to not have to explicitely implement every data constructors string representation.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
John Smith
  • 2,282
  • 1
  • 14
  • 22

3 Answers3

7

For a type with a Data.Data.Data instance, this function is easy: it's merely

showConstr . toConstr :: Data a => a -> String

For example,

Prelude Data.Data> showConstr . toConstr $ Just 5
"Just"

For a type which doesn't implement Data, it is fairly hopeless, because you can't look inside the type to see how it's implemented. But since you define these types yourself, you can merely ensure they have a Data instance. It is derived automatically with deriving Data, provided you have enabled DeriveDataTypeable.

Note that Data is only suitable for types which are algebraic and transparent through and through. You won't be able to derive an instance for a type containing, say, a function in one of its fields. So this may not be as much of a reprieve from the tyranny of Show as you'd hoped: a lot of the types Show can't support will also be rejected by Data. Generic may provide a more general solution. I'm no expert on generics, but conNameOf looks promising.

amalloy
  • 89,153
  • 8
  • 140
  • 205
2

A more explicit approach is to create a custom derivation via TemplateHaskell. The following code describes the logic for generating custom Show instances for a given datatype:

genShow :: Name -> Q [Dec]
genShow typName =
  do  -- Getting type definition
     (TyConI d) <- reify typName -- Get all the information on the type

     -- Extracting interesting info: type name, args and constructors
     let unpackConstr c = case c of
           NormalC cname args -> (cname, length args)
           InfixC _ cname _ -> (cname, 2)
           RecC cname args -> (cname, length args)
           ForallC _ _ c -> unpackConstr c
           _ -> error "you need to figure out GADTs yourself"

     (type_name, targs, constructors) <-
       case d of
         d@(DataD _ name targs _ cs _) ->
           return (name, targs, map unpackConstr cs)
         d@(NewtypeD _ name targs _ con _) ->
           return (name, targs, [unpackConstr con])
         _ -> error ("derive: not a data type declaration: " ++ show d)

     -- Extracting name from type args
     let targName targ = case targ of
           PlainTV tvname _ -> tvname
           KindedTV tvname _ _ -> tvname

     -- Manually building AST for an instance. 
     -- Essentially, we match on every constructor and make our `show`
     -- return it as a string result.
     i_dec <- instanceD (cxt [])
       (appT (conT (mkName "Show")) (foldl appT (conT type_name) 
         (map (varT . targName) targs)))
       [funD (mkName "show") (flip map constructors $ \constr ->
         let myArgs = [conP (fst constr) $ map (const wildP) [1..snd constr]]
             myBody = normalB $ stringE $ nameBase $ fst constr
         in clause myArgs myBody []
       )]
     return [i_dec]

Then, you simply do

data MyData = D Int | X

$(genShow ''MyData)

...and you can happily show it. Note that both code snippets must be placed in separate modules and you need to use TemplateHaskell extension.


I took a lot of inspiration from this article.

radrow
  • 6,419
  • 4
  • 26
  • 53
  • 1
    But seriously, go for @amalloy 's version in your case – radrow Jan 18 '22 at 13:09
  • quite frankly I like your approach better, because I want a proper general approach because I simply want it to work in every case... I had to change one thing for it to work. the `(\NormalC cname args) -> ...` lambda gave me a pattern matching error, so I turned it into a proper function and added the pattern `(RecC cname args)`... but, now I ran into another problem... I'm not able to call this template function on a type that has type arguments... but I'm guessing that's because I have no idea about how template haskell works lol – John Smith Jan 24 '22 at 06:56
  • I'll fix that example for you then – radrow Jan 24 '22 at 08:14
  • 1
    @JohnSmith done, should work for everything except GADTs (exercise for the reader) – radrow Jan 24 '22 at 09:17
1

You could derive via

-- >> Anonymous 120320
-- Anonymous
-- >> User "Iðunn" 242424
-- User
data User
 = User String Int
 | Anonymous Int
 deriving
 stock Generic

 deriving Show
 via OnlyConstructors User

given

type    OnlyConstructors :: Type -> Type
newtype OnlyConstructors a = OnlyConstructors a

instance (Generic a, GNames (GHC.Generics.Rep a)) => Show (OnlyConstructors a) where
  showsPrec :: Int -> OnlyConstructors a -> ShowS
  showsPrec _ (OnlyConstructors a) = gnames (from a)

type  GNames :: (Type -> Type) -> Constraint
class GNames rep where
  gnames :: rep () -> ShowS

instance GNames rep => GNames (D1 meta rep) where
  gnames :: D1 meta rep () -> ShowS
  gnames (M1 rep) = gnames rep

instance GNames V1 where
  gnames :: V1 () -> ShowS
  gnames = \case

instance (GNamesProd rep, GNames rep') => GNames (rep :+: rep') where
  gnames :: (rep :+: rep') () -> ShowS
  gnames (L1 as) = gnamesProd as
  gnames (R1 bs) = gnames bs

instance GNamesProd (C1 meta rep) => GNames (C1 meta rep) where
  gnames :: C1 meta rep () -> ShowS
  gnames = gnamesProd

type  GNamesProd :: (Type -> Type) -> Constraint
class GNamesProd rep where
  gnamesProd :: rep () -> ShowS

instance (KnownSymbol cons, meta ~ MetaCons cons fixity sel) => GNamesProd (C1 meta rep) where
  gnamesProd :: C1 (MetaCons cons fixity sel) rep () -> ShowS
  gnamesProd (M1 as) = showString (symbolVal @cons Proxy)
Iceland_jack
  • 6,848
  • 7
  • 37
  • 46