Search code examples
haskellgenericsderiving

Alternative of Show that only uses name


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.


Solution

  • 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.