th-utilities-0.2.0.0: src/TH/ReifySimple.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
-- | Utilities for reifying simplified datatype info. It omits details
-- that aren't usually relevant to generating instances that work with
-- the datatype. This makes it easier to use TH to derive instances.
--
-- The \"Simple\" in the module name refers to the simplicity of the
-- datatypes, not the module itself, which exports quite a few things
-- which are useful in some circumstance or another. I anticipate that
-- the most common uses of this will be the following APIs:
--
-- * Getting info about a @data@ or @newtype@ declaration, via
-- 'DataType', 'reifyDataType', and 'DataCon'. This is useful for
-- writing something which generates declarations based on a datatype,
-- one of the most common uses of Template Haskell.
--
-- * Getting nicely structured info about a named type. See 'TypeInfo'
-- and 'reifyType'. This does not yet support reifying typeclasses,
-- primitive type constructors, or type variables ('TyVarI').
--
-- Currently, this module supports reifying simplified versions of the
-- following 'Info' constructors:
--
-- * 'TyConI' with 'DataD' and 'NewtypeD' (becomes a 'DataType' value)
--
-- * 'FamilyI' becomes a 'DataFamily' or 'TypeFamily' value.
--
-- * 'DataConI' becomes a 'DataCon' value.
--
-- In the future it will hopefully also have support for the remaining
-- 'Info' constructors, 'ClassI', 'ClassOpI', 'PrimTyConI', 'VarI', and
-- 'TyVarI'.
module TH.ReifySimple
(
-- * Reifying simplified type info
TypeInfo, reifyType, infoToType
, reifyTypeNoDataKinds, infoToTypeNoDataKinds
-- * Reifying simplified info for specific declaration varieties
-- ** Datatype info
, DataType(..), reifyDataType, infoToDataType
-- ** Data constructor info
, DataCon(..), reifyDataCon, infoToDataCon, typeToDataCon
-- ** Data family info
, DataFamily(..), DataInst(..), reifyDataFamily, infoToDataFamily
-- ** Type family info
, TypeFamily(..), TypeInst(..), reifyTypeFamily, infoToTypeFamily
-- * Other utilities
, conToDataCons
, reifyDataTypeSubstituted
) where
import Control.Applicative ((<|>))
import Data.Data (Data, gmapT)
import Data.Generics.Aliases (extT)
import qualified Data.Map as M
import Data.Typeable (Typeable)
import GHC.Generics (Generic)
import Language.Haskell.TH
import Language.Haskell.TH.Instances ()
import TH.Utilities
data TypeInfo
= DataTypeInfo DataType
| DataFamilyInfo DataFamily
| TypeFamilyInfo TypeFamily
| LiftedDataConInfo DataCon
-- | Reifies a 'Name' as a 'TypeInfo', and calls 'fail' if this doesn't
-- work. Use 'reify' with 'infoToType' if you want to handle the failure
-- case more gracefully.
--
-- This does not yet support reifying typeclasses, primitive type
-- constructors, or type variables ('TyVarI').
reifyType :: Name -> Q TypeInfo
reifyType name = do
info <- reify name
mres <- infoToType info
case mres of
Just res -> return res
Nothing -> fail $
"Expected to reify a data type, data family, or type family. Instead got:\n" ++
pprint info
-- | Convert an 'Info' into a 'TypeInfo' if possible, and otherwise
-- yield 'Nothing'. Needs to run in 'Q' so that
infoToType :: Info -> Q (Maybe TypeInfo)
infoToType info =
case (infoToTypeNoDataKinds info, infoToDataCon info) of
(Just result, _) -> return (Just result)
(Nothing, Just dc) -> do
#if MIN_VERSION_template_haskell(2,11,0)
dataKindsEnabled <- isExtEnabled DataKinds
#else
reportWarning $
"For " ++ pprint (dcName dc) ++
", assuming DataKinds is on, and yielding LiftedDataConInfo."
let dataKindsEnabled = True
#endif
return $ if dataKindsEnabled then Just (LiftedDataConInfo dc) else Nothing
(Nothing, Nothing) -> return Nothing
-- | Reifies type info, but instead of yielding a 'LiftedDataConInfo',
-- will instead yield 'Nothing'.
reifyTypeNoDataKinds :: Name -> Q (Maybe TypeInfo)
reifyTypeNoDataKinds = fmap infoToTypeNoDataKinds . reify
-- | Convert an 'Info into a 'TypeInfo' if possible. If it's a data
-- constructor, instead of yielding 'LiftedDataConInfo', it will instead
-- yield 'Nothing'.
infoToTypeNoDataKinds :: Info -> Maybe TypeInfo
infoToTypeNoDataKinds info =
(DataTypeInfo <$> infoToDataType info) <|>
(DataFamilyInfo <$> infoToDataFamily info) <|>
(TypeFamilyInfo <$> infoToTypeFamily info)
--------------------------------------------------------------------------------
-- Reifying specific declaration varieties
-- | Simplified info about a 'DataD'. Omits deriving, strictness,
-- kind info, and whether it's @data@ or @newtype@.
data DataType = DataType
{ dtName :: Name
, dtTvs :: [Name]
, dtCxt :: Cxt
, dtCons :: [DataCon]
} deriving (Eq, Show, Ord, Data, Typeable, Generic)
-- | Simplified info about a 'Con'. Omits deriving, strictness, and kind
-- info. This is much nicer than consuming 'Con' directly, because it
-- unifies all the constructors into one.
data DataCon = DataCon
{ dcName :: Name
, dcTvs :: [Name]
, dcCxt :: Cxt
, dcFields :: [(Maybe Name, Type)]
} deriving (Eq, Show, Ord, Data, Typeable, Generic)
-- | Simplified info about a data family. Omits deriving, strictness, and
-- kind info.
data DataFamily = DataFamily
{ dfName :: Name
, dfTvs :: [Name]
, dfInsts :: [DataInst]
} deriving (Eq, Show, Ord, Data, Typeable, Generic)
-- | Simplified info about a data family instance. Omits deriving,
-- strictness, and kind info.
data DataInst = DataInst
{ diName :: Name
, diCxt :: Cxt
, diParams :: [Type]
, diCons :: [DataCon]
} deriving (Eq, Show, Ord, Data, Typeable, Generic)
-- | Simplified info about a type family. Omits kind info and injectivity
-- info.
data TypeFamily = TypeFamily
{ tfName :: Name
, tfTvs :: [Name]
, tfInsts :: [TypeInst]
} deriving (Eq, Show, Ord, Data, Typeable, Generic)
-- | Simplified info about a type family instance. Omits nothing.
data TypeInst = TypeInst
{ tiName :: Name
, tiParams :: [Type]
, tiType :: Type
} deriving (Eq, Show, Ord, Data, Typeable, Generic)
-- | Reify the given data or newtype declaration, and yields its
-- 'DataType' representation.
reifyDataType :: Name -> Q DataType
reifyDataType name = do
info <- reify name
case infoToDataType info of
Nothing -> fail $ "Expected to reify a datatype. Instead got:\n" ++ pprint info
Just x -> return x
-- | Reify the given data constructor.
reifyDataCon :: Name -> Q DataCon
reifyDataCon name = do
info <- reify name
case infoToDataCon info of
Nothing -> fail $ "Expected to reify a constructor. Instead got:\n" ++ pprint info
Just x -> return x
-- | Reify the given data family, and yield its 'DataFamily'
-- representation.
reifyDataFamily :: Name -> Q DataFamily
reifyDataFamily name = do
info <- reify name
case infoToDataFamily info of
Nothing -> fail $ "Expected to reify a data family. Instead got:\n" ++ pprint info
Just x -> return x
-- | Reify the given type family instance declaration, and yields its
-- 'TypeInst' representation.
reifyTypeFamily :: Name -> Q TypeFamily
reifyTypeFamily name = do
info <- reify name
case infoToTypeFamily info of
Nothing -> fail $ "Expected to reify a type family. Instead got:\n" ++ pprint info
Just x -> return x
infoToDataType :: Info -> Maybe DataType
infoToDataType info = case info of
#if MIN_VERSION_template_haskell(2,11,0)
TyConI (DataD preds name tvs _kind cons _deriving) ->
#else
TyConI (DataD preds name tvs cons _deriving) ->
#endif
Just $ DataType name (map tyVarBndrName tvs) preds (concatMap conToDataCons cons)
#if MIN_VERSION_template_haskell(2,11,0)
TyConI (NewtypeD preds name tvs _kind con _deriving) ->
#else
TyConI (NewtypeD preds name tvs con _deriving) ->
#endif
Just $ DataType name (map tyVarBndrName tvs) preds (conToDataCons con)
_ -> Nothing
infoToDataFamily :: Info -> Maybe DataFamily
infoToDataFamily info = case info of
#if MIN_VERSION_template_haskell(2,11,0)
FamilyI (DataFamilyD name tvs _kind) insts ->
#else
FamilyI (FamilyD DataFam name tvs _kind) insts ->
#endif
Just $ DataFamily name (map tyVarBndrName tvs) (map go insts)
_ -> Nothing
where
#if MIN_VERSION_template_haskell(2,11,0)
go (NewtypeInstD preds name params _kind con _deriving) =
#else
go (NewtypeInstD preds name params con _deriving) =
#endif
DataInst name preds params (conToDataCons con)
#if MIN_VERSION_template_haskell(2,11,0)
go (DataInstD preds name params _kind cons _deriving) =
#else
go (DataInstD preds name params cons _deriving) =
#endif
DataInst name preds params (concatMap conToDataCons cons)
go info' = error $
"Unexpected instance in FamilyI in infoToDataInsts:\n" ++ pprint info'
infoToTypeFamily :: Info -> Maybe TypeFamily
infoToTypeFamily info = case info of
#if MIN_VERSION_template_haskell(2,11,0)
FamilyI (ClosedTypeFamilyD (TypeFamilyHead name tvs _result _injectivity) eqns) _ ->
Just $ TypeFamily name (map tyVarBndrName tvs) $ map (goEqn name) eqns
FamilyI (OpenTypeFamilyD (TypeFamilyHead name tvs _result _injectivity)) insts ->
Just $ TypeFamily name (map tyVarBndrName tvs) $ map go insts
#else
FamilyI (ClosedTypeFamilyD name tvs _kind eqns) [] ->
Just $ TypeFamily name (map tyVarBndrName tvs) $ map (goEqn name) eqns
FamilyI (FamilyD TypeFam name tvs _kind) insts ->
Just $ TypeFamily name (map tyVarBndrName tvs) $ map go insts
#endif
_ -> Nothing
where
goEqn name (TySynEqn params ty) = TypeInst name params ty
go (TySynInstD name (TySynEqn params ty)) = TypeInst name params ty
go info' = error $
"Unexpected instance in FamilyI in infoToTypeInsts:\n" ++ pprint info'
infoToDataCon :: Info -> Maybe DataCon
infoToDataCon info = case info of
#if MIN_VERSION_template_haskell(2,11,0)
DataConI name ty _parent ->
#else
DataConI name ty _parent _fixity ->
#endif
Just (typeToDataCon name ty)
_ -> Nothing
-- | Creates a 'DataCon' given the 'Name' and 'Type' of a
-- data-constructor. Note that the result the function type is *not* checked to match the provided 'Name'.
typeToDataCon :: Name -> Type -> DataCon
typeToDataCon dcName ty0 = DataCon {..}
where
(dcTvs, dcCxt, dcFields) = case ty0 of
ForallT tvs preds ty -> (map tyVarBndrName tvs, preds, typeToFields ty)
ty -> ([], [], typeToFields ty)
-- TODO: Should we sanity check the result type?
typeToFields = init . map (Nothing, ) . unAppsT
-- | Convert a 'Con' to a list of 'DataCon'. The result is a list
-- because 'GadtC' and 'RecGadtC' can define multiple constructors.
conToDataCons :: Con -> [DataCon]
conToDataCons = \case
NormalC name slots ->
[DataCon name [] [] (map (\(_, ty) -> (Nothing, ty)) slots)]
RecC name fields ->
[DataCon name [] [] (map (\(n, _, ty) -> (Just n, ty)) fields)]
InfixC (_, ty1) name (_, ty2) ->
[DataCon name [] [] [(Nothing, ty1), (Nothing, ty2)]]
ForallC tvs preds con ->
map (\(DataCon name tvs0 preds0 fields) ->
DataCon name (tvs0 ++ map tyVarBndrName tvs) (preds0 ++ preds) fields) (conToDataCons con)
#if MIN_VERSION_template_haskell(2,11,0)
GadtC ns slots _ ->
map (\n -> DataCon n [] [] (map (\(_, ty) -> (Nothing, ty)) slots)) ns
RecGadtC ns fields _ ->
map (\n -> DataCon n [] [] (map (\(n, _, ty) -> (Just n, ty)) fields)) ns
#endif
-- | Like 'reifyDataType', but takes a 'Type' instead of just the 'Name'
-- of the datatype. It expects a normal datatype argument (see
-- 'typeToNamedCon').
reifyDataTypeSubstituted :: Type -> Q DataType
reifyDataTypeSubstituted ty =
case typeToNamedCon ty of
Nothing -> fail $ "Expected a datatype, but reifyDataTypeSubstituted was applied to " ++ pprint ty
Just (n, args) -> do
dt <- reifyDataType n
let cons' = substituteTvs (M.fromList (zip (dtTvs dt) args)) (dtCons dt)
return (dt { dtCons = cons' })
-- TODO: add various handy generics based traversals to TH.Utilities
substituteTvs :: Data a => M.Map Name Type -> a -> a
substituteTvs mp = transformTypes go
where
go (VarT name) | Just ty <- M.lookup name mp = ty
go ty = gmapT (substituteTvs mp) ty
transformTypes :: Data a => (Type -> Type) -> a -> a
transformTypes f = gmapT (transformTypes f) `extT` (id :: String -> String) `extT` f