Using generics to generate documentation about newtype

Viewed 101

I have a Haskell project, which uses a few newtypes. I would like to export a table of these, so I can include it in my documentation (not-haddock), as a markdown table for instance. I'm not familiar with this, but from reading around, my plan was to use Generics/Data.Data, to create some functions that broadly look as follows:

data MyRowRepresentation = MyRowRepresentation
                                String -- Name of the newtype
                                String -- Name of the type its wrapping

toDocRow :: (Generic a, HasDatatypeInfo a) -> (Proxy a) -> MyRowRepresentation
toDocRow = <the part I'm struggling with>

newtype MyType0 = MyType0 Int16
newtype MyType1 = MyType0 Int8
newtype MyType2 = MyType0 Int32
newtype MyType3 = MyType0 Word8

main = do
  let table = [
        toDocRow (Proxy::MyType0),
        toDocRow (Proxy::MyType1),
        toDocRow (Proxy::MyType2),
        toDocRow (Proxy::MyType3),
        ]
  -- Do something to write `table` to disk.

However, I am struggling to understand how Generics/Data.Data interacts with Constructors. Its my first time using area of Haskell, so I tried the following code, just to see how it worked:

{-# LANGUAGE DeriveGeneric #-}
module TmpTmpExport where

import RIO
import RIO.List.Partial (head)
import Text.Show.Pretty
import Data.String.Conversions    (cs)
import Data.Data


newtype MyNewType = MyNewType Integer
  deriving (Show)
  deriving (Data, Typeable)

main :: IO ()
main = do
  traceM $ cs $ "INSTANCE: "            ++ (show $ dataTypeConstrs $ dataTypeOf $ (MyNewType 0))
  traceM $ cs $ "INSTANCE constrType: " ++ (show $ constrType $ head $ dataTypeConstrs $ dataTypeOf $ (MyNewType 0))
  traceM $ cs $ "INSTANCE constrType: " ++ (show $ dataTypeConstrs $ constrType $ head $ dataTypeConstrs $ dataTypeOf $ (MyNewType 0))
  traceM $ cs $ "INSTANCE constrType: " ++ (show $ (fmap showConstr) $ dataTypeConstrs $ constrType $ head $ dataTypeConstrs $ dataTypeOf $ (MyNewType 0))
  traceM $ cs $ "INSTANCE constrType: " ++ (show $ dataTypeConstrs $ constrType $ head $ dataTypeConstrs $ constrType $ head $ dataTypeConstrs $ constrType $ head $ dataTypeConstrs $ dataTypeOf $ (MyNewType 0))



  return ()

Which gives the following output:

INSTANCE: [MyNewType]
INSTANCE constrType: DataType {tycon = "MyNewType", datarep = AlgRep [MyNewType]}
INSTANCE constrType: [MyNewType]
INSTANCE constrType: ["MyNewType"]
INSTANCE constrType: [MyNewType]

I don't think I'm a million miles away, but from reading the documentation for Data.Data, and trying various functions out, I couldn't work out how to "get inside" the newtype using dataTypeConstrs and constrType, so that I can create table with rows like:

table = [
    (MyRowRepresentation "Type0" "Int16"),
    (MyRowRepresentation "Type1" "Int8")
    (MyRowRepresentation "Type2" "Int32")
    (MyRowRepresentation "Type3" "Word8")
]

Have I conceptually missed something about the difference between newtype and data, is this possible to do with Data.Data, or should I be looking at a different tool/library?

1 Answers

You can definitely do this with GHC.Generics. I'm less familiar with Data.Data.

The easy way, using coercible-utils >= 0.1.0:

import CoercibleUtils.Newtype

toDocRow :: forall a o proxy. (Newtype a o, Typeable a, Typeable o) => proxy a -> MyRowRepresentation
toDocRow _ = MyRowRepresentation
  (show (typeRep (Proxy :: Proxy a)))
  (show (typeRep (Proxy :: Proxy o)))

This version insists on an actual newtype, rather than a single-constructor datatype.


Here's how you could roll your own with Generic. The instances will be for M1 and K1. My first thought is that you should use Typeable on the very outside (before you open up the generic representation) to get the newtype name, then use a boring instance to ignore M1 layers, then use Typeable again for K1 to get the wrapped type info. On the outside you could instead use the metadata attached to the outermost M1, but that seems much more awkward.

Note that the below can be cleaned up a bit using TypeApplications instead of proxy passing.

{-# language DeriveGeneric, FlexibleContexts, ScopedTypeVariables, KindSignatures #-}
import GHC.Generics
import Data.Typeable
import Data.Int
import Data.Word
import Data.Kind (Type)

data MyRowRepresentation = MyRowRepresentation
  String -- Name of the newtype
  String -- Name of the type its wrapping
  deriving Show

toDocRow :: forall a proxy. (Generic a, Typeable a, Grump (Rep a)) => proxy a -> MyRowRepresentation
toDocRow _ = MyRowRepresentation
  (show (typeRep (Proxy :: Proxy a)))
  (show $ grump (Proxy :: Proxy (Rep a)))

class Grump (f :: Type -> Type) where
  grump :: Proxy f -> TypeRep

instance Grump f => Grump (M1 i c f) where
  grump _ = grump (Proxy :: Proxy f)

instance Typeable c => Grump (K1 i c) where
  grump _ = typeRep (Proxy :: Proxy c)

newtype MyType0 = MyType0 Int16
  deriving Generic
newtype MyType1 = MyType1 Int8
  deriving Generic
newtype MyType2 = MyType2 Int32
  deriving Generic
newtype MyType3 = MyType3 Word8
  deriving Generic

main = do
  let table = [
        toDocRow (Proxy:: Proxy MyType0),
        toDocRow (Proxy:: Proxy MyType1),
        toDocRow (Proxy:: Proxy MyType2),
        toDocRow (Proxy:: Proxy MyType3)
        ]
  print table
Related