interpolator-1.1.0.1: src/Data/Interpolation/TH.hs
module Data.Interpolation.TH
( makeInterpolatorSumInstance
, withUninterpolated
, withPolymorphic
, deriveUninterpolated
) where
import Prelude
import Data.Char (toLower)
import Data.Either.Validation (Validation (Success))
import Data.List (dropWhileEnd)
import Data.Profunctor.Product.Default (Default, def)
import Data.Semigroup ((<>))
import Data.Sequences (catMaybes, replicateM, singleton, stripPrefix)
import Data.Traversable (for)
import Language.Haskell.TH
( Con (NormalC, RecC)
, Dec (DataD, NewtypeD, TySynD)
, Info (TyConI)
, Name
, Q
, Type (AppT, ConT, VarT)
, isInstance
, lookupTypeName
, mkName
, nameBase
, newName
, pprint
, reify
, reportError
)
import qualified Language.Haskell.TH.Lib as TH
import Language.Haskell.TH.Syntax (returnQ)
import Data.Interpolation (FromTemplateValue, Interpolator (Interpolator), runInterpolator)
extractSumConstructorsAndNumFields :: Name -> Q [(Name, Int)]
extractSumConstructorsAndNumFields ty = do
reify ty >>= \ case
TyConI (NewtypeD _ _ _ _ c _) -> singleton <$> extractConstructor c
TyConI (DataD _ _ _ _ cs _) -> traverse extractConstructor cs
other -> fail $ "can't extract constructors: " <> show other
where
extractConstructor = \ case
NormalC n fs -> pure (n, length fs)
other -> fail $ "won't extract constructors: " <> show other <> " - sum types only"
-- |Make an instance of 'Default' for 'Interpolator' of an ADT. Can't do it for an arbitrary
-- Profunctor p because of partial functions. This splice is meant to be used in conjunction with
-- 'makeAdaptorAndInstance' for records as a way to project 'Default' instances down to all leaves.
--
-- @
--
-- data Foo' a b = Foo1 a | Foo2 b
-- makeInterpolatorSumInstance ''Foo'
--
-- @
--
-- @
--
-- instance (Default Interpolator a1 b1, Default Interpolator a2 b2) => Default Interpolator (Foo' a1 a2) (Foo' b1 b2) where
-- def = Interpolator $ \ case
-- Foo1 x -> Foo1 <$> runInterpolator def x
-- Foo2 x -> Foo2 <$> runInterpolator def x
--
-- @
makeInterpolatorSumInstance :: Name -> Q [Dec]
makeInterpolatorSumInstance tyName = do
cs <- extractSumConstructorsAndNumFields tyName
(contextConstraints, templateVars, identityVars) <- fmap (unzip3 . mconcat) $ for cs $ \ (_, i) -> replicateM i $ do
a <- newName "a"
b <- newName "b"
pure ([t| Default Interpolator $(TH.varT a) $(TH.varT b) |], a, b)
let appConstructor x y = TH.appT y (TH.varT x)
templateType = foldr appConstructor (TH.conT tyName) templateVars
identityType = foldr appConstructor (TH.conT tyName) identityVars
matches = flip fmap cs $ \ (c, i) -> case i of
0 -> TH.match (TH.conP c []) (TH.normalB [| pure $ Success $(TH.conE c) |]) []
1 -> do
x <- newName "x"
TH.match (TH.conP c [TH.varP x]) (TH.normalB [| fmap $(TH.conE c) <$> runInterpolator def $(TH.varE x) |]) []
_ -> fail "can only match sum constructors up to 1 argument"
sequence
[ TH.instanceD
(TH.cxt contextConstraints)
[t| Default Interpolator $(templateType) $(identityType) |]
[ TH.funD
'def
[TH.clause [] (TH.normalB [| Interpolator $(TH.lamCaseE matches) |]) []]
]
]
-- |When applied to a simple data type declaration, substitute a fully-polymorphic data type
-- (suffixed with a "prime"), and type aliases for "normal" and "uninterpolated" variants.
--
-- For example, a record or newtype (using record syntax):
--
-- @
-- withUninterpolated [d|
-- data Foo = Foo
-- { fooBar :: String
-- , fooBaz :: Maybe Int
-- } deriving (Eq, Show)
-- |]
-- @
--
-- Is equivalent to:
--
-- @
-- data Foo' bar baz = Foo
-- { fooBar :: bar
-- , fooBaz :: baz
-- } deriving (Eq, Show)
-- type Foo = Foo' String (Maybe Int)
-- type UninterpolatedFoo = Foo' (Uninterpolated String) (Maybe (Uninterpolated Int))
-- @
--
-- __Note:__ the trailing @|]@ of the quasi quote bracket has to be indented or a parse error will occur.
--
-- A simple sum type whose constructors have one argument or less:
--
-- @
-- withUninterpolated [d|
-- data MaybeFoo
-- = AFoo Foo
-- | NoFoo
-- deriving (Eq, Show)
-- @
--
-- Expands to:
--
-- @
-- data MaybeFoo' aFoo
-- = AFoo aFoo
-- | NoFoo
-- deriving (Eq, Show)
-- type MaybeFoo = MaybeFoo' Foo
-- type UninterpolatedMaybeFoo = MaybeFoo' (Foo' (Uninterpolated String) (Maybe (Uninterpolated Int)))
-- -- Note: UninterpolatedMaybeFoo ~ MaybeFoo' UninterpolatedFoo
-- @
--
-- Whenever the type of a field is one for which an instance of 'FromTemplateValue' is present, the
-- type is wrapped in 'Uninterpolated'. Otherwise, an attempt is made to push 'Uninterpolated' down
-- into the field's type, even if it's a type synonym such as one generated by this same macro.
--
-- Note: this splice is equivalent to @withPolymorphic [d|data Foo ... |]@ followed by
-- @deriveUninterpolated ''Foo@.
withUninterpolated :: Q [Dec] -> Q [Dec]
withUninterpolated qDecs = do
(poly, simple) <- withPolymorphic_ qDecs
uninterp <- deriveUninterpolated_ simple
pure $ [poly, simple] <> uninterp
-- |When applied to a simple data type declaration, substitute a fully-polymorphic data type
-- (suffixed with a "prime"), and a simple type alias which matches the supplied declaration.
--
-- This splice does not include the corresponding "Uninterpolated" type, so it can be used separately
-- when needed. For example, if you want to define all your record types first, then define/derive
-- the Uninterpolated types for each. This can be important because the presence of a
-- 'FromTemplateValue' instance, defined before the splice, will affect the shape of the derived
-- Uninterpolated type.
--
-- For example, a record or newtype (using record syntax):
--
-- @
-- withPolymorphic [d|
-- data Foo = Foo
-- { fooBar :: String
-- , fooBaz :: Maybe Int
-- } deriving (Eq, Show)
-- |]
-- @
--
-- Is equivalent to:
--
-- @
-- data Foo' bar baz = Foo
-- { fooBar :: bar
-- , fooBaz :: baz
-- } deriving (Eq, Show)
-- type Foo = Foo' String (Maybe Int)
-- @
--
-- __Note:__ the trailing @|]@ of the quasi quote bracket has to be indented or a parse error will occur.
withPolymorphic :: Q [Dec] -> Q [Dec]
withPolymorphic qDecs = do
(poly, simple) <- withPolymorphic_ qDecs
pure [poly, simple]
-- |Given the name of a type alias which specializes a polymorphic type (such as the "simple" type
-- generated by 'withPolymorphic'), generate the corresponding "Uninterpolated" type alias which
-- replaces each simple type with an 'Uninterpolated' form, taking account for which types have
-- 'FromTemplateValue' instances.
--
-- Use this instead of 'withUninterpolated' when you need to define instances for referenced types,
-- and you need flexibility in the ordering of declarations in your module's source.
deriveUninterpolated :: Name -> Q [Dec]
deriveUninterpolated tName =
reify tName >>= \ case
TyConI dec -> deriveUninterpolated_ dec
other -> do
reportError $ "Can't handle type: " <> show other <> "; expected a \"simple\" type alias"
pure []
---------------
-- * Internal
-- |From a simple type declaration, generate declarations for the polymorphic type and the simple
-- type alias.
withPolymorphic_ :: Q [Dec] -> Q (Dec, Dec)
withPolymorphic_ qDecs = do
decs <- qDecs
case decs of
-- "data" with a single record constructor:
[DataD [] tName [] Nothing [RecC cName fields] deriv] -> do
let con = TH.recC (simpleName cName) (returnQ <$> (fieldToPolyField tName <$> fields))
primedDecl <- TH.dataD (pure []) (primedName tName) (fieldToTypeVar tName <$> fields) Nothing [con] (returnQ <$> deriv)
normalSyn <- TH.tySynD (simpleName tName) [] $
returnQ $ foldl (\ t v -> AppT t (fieldToSimpleType v)) (ConT (primedName tName)) fields
pure (primedDecl, normalSyn)
-- "newtype" with a single record constructor:
[NewtypeD [] tName [] Nothing (RecC cName [field]) deriv] -> do
-- TODO: use the type name, lower-cased, instead of the field name, for the type var?
let con = TH.recC (simpleName cName) (returnQ <$> [fieldToPolyField tName field])
primedDecl <- TH.newtypeD (pure []) (primedName tName) [fieldToTypeVar tName field] Nothing con (returnQ <$> deriv)
normalSyn <- TH.tySynD (simpleName tName) [] $
returnQ $ AppT (ConT (primedName tName)) (fieldToSimpleType field)
pure (primedDecl, normalSyn)
-- "data" with multiple simple constructors:
[DataD [] tName [] Nothing constrs deriv] -> do
let mapConstr = \ case
NormalC cName [(s, t)] ->
let vName = niceName tName cName
in pure (Just (TH.plainTV vName), NormalC cName [(s, VarT vName)], Just t)
NormalC cName [] ->
pure (Nothing, NormalC cName [], Nothing)
other -> fail $ "Can't handle constructor: " <> pprint other
(vars, constrs', ts) <- unzip3 <$> traverse mapConstr constrs
primedDecl <- TH.dataD (pure []) (primedName tName) (catMaybes vars) Nothing (returnQ <$> constrs') (returnQ <$> deriv)
normalSyn <- TH.tySynD (simpleName tName) [] $
returnQ $ foldl AppT (ConT (primedName tName)) (catMaybes ts)
pure (primedDecl, normalSyn)
_ -> do
fail $ "Can't handle declaration: " <> pprint decs
where
-- The same name, with a "'" added to the end:
primedName n = mkName (nameBase n <> "'")
-- The same name, in a fresh context:
simpleName = mkName . nameBase
-- Remove leading "_" and type name, if either is present:
unPrefixedFieldName tName = mkName . avoidKeywords . unCap . stripped (unCap $ nameBase tName) . stripped "_" . nameBase
fieldToTypeVar tName (fName, _, _) = TH.plainTV (unPrefixedFieldName tName fName)
fieldToPolyField tName (fName, s, _) = (simpleName fName, s, VarT (unPrefixedFieldName tName fName))
fieldToSimpleType (_, _, t) = t
niceName prefix = mkName . avoidKeywords. unCap . stripped (nameBase prefix) . nameBase
avoidKeywords str = if str `elem` likelyKeywords then str <> "_" else str
where
likelyKeywords =
[ "as", "case", "class", "data", "default", "deriving", "do", "else", "family", "forall"
, "foreign", "if", "in", "import", "infix", "infixl", "infixr", "instance", "hiding"
, "let", "mdo", "module", "newtype", "of", "proc", "qualified", "rec", "then", "type", "where"
]
stripped prefix str = maybe str id (stripPrefix prefix str)
unCap = \ case
c : cs -> toLower c : cs
other -> other
deriveUninterpolated_ :: Dec -> Q [Dec]
deriveUninterpolated_ dec = do
case dec of
TySynD sName [] typ -> do
uninterp <- TH.tySynD (mkName $ "Uninterpolated" <> nameBase sName) [] (mapUninterp typ)
pure [uninterp]
_ -> do
reportError $ "Can't handle declaration: " <> show dec <> "; expected a \"simple\" type alias"
pure []
-- Apply the Uninterpolated constructor, pushing it inside an outer type application, and into
-- every type parameter, even in the presence of type synonyms (such as those generated here.)
-- If there is a 'FromTemplateValue' instance for an argument type, that constructor is applied
-- to that type, with its structure left intact.
mapUninterp :: Type -> Q Type
mapUninterp typ = do
uninterp <- lookupTypeName "Uninterpolated" >>= maybe (fail "Uninterpolated not in scope") returnQ
let wrap = AppT (ConT uninterp)
-- Apply only to the _right_ side of (nested) AppTs:
mapRight :: Type -> Q Type
mapRight = \ case
AppT t1@(AppT _ _) t2 -> AppT <$> mapRight t1 <*> mapOne t2
AppT t1 t2 -> AppT t1 <$> mapOne t2
t -> mapOne t
mapOne :: Type -> Q Type
mapOne t = do
mapped <- isInstance ''FromTemplateValue [t] >>= \ case
True -> pure $ wrap t
False -> case t of
ConT n -> do
info <- reify n
case info of
TyConI (DataD _ _ _ _ _ _) -> pure (ConT n)
TyConI (NewtypeD _ _ _ _ _ _) -> pure (ConT n)
TyConI (TySynD _ [] t1) -> mapOne t1
other -> do
reportError $ "Can't handle constructor: " <> pprint other
pure $ ConT n
t1@(AppT _ _) -> mapRight t1
other -> do
reportError $ "Can't handle type: " <> pprint other
pure other
lookupAlias mapped >>= \ case
Just uName -> pure $ ConT uName
Nothing -> pure mapped
-- Name of the "Uninterpolated..." alias which is already in scope and exactly matches
-- the given type, if any. This prevents the type aliases for nested types from getting
-- out of hand, both in Haddock and in compile errors.
lookupAlias :: Type -> Q (Maybe Name)
lookupAlias t =
case constrName t of
Nothing -> pure Nothing
Just cName -> do
let uninterpName = "Uninterpolated" <> dropWhileEnd (== '\'') (nameBase cName)
lookupTypeName uninterpName >>= \ case
Nothing -> pure Nothing
Just uName -> do
uInfo <- reify uName
case uInfo of
TyConI (TySynD _ [] namedT) | namedT == t -> do
pure $ Just uName
_ -> do
pure Nothing
-- Name of the constructor at the bottom-left of a chain of AppT; that is, the type
-- constructor being applied to a series of argument types, if that's what it looks like.
constrName :: Type -> Maybe Name
constrName = \ case
ConT name -> Just name
AppT t _ -> constrName t
_ -> Nothing
mapRight typ