type-machine-0.1.0.0: src/TypeMachine/TH/Is.hs
module TypeMachine.TH.Is (isClassName, deriveIs, defineIs) where
import Control.Monad (MonadPlus (mzero), forM)
import qualified Data.Map.Strict as Map
import Language.Haskell.TH hiding (Type, reifyType)
import qualified Language.Haskell.TH as TH
import Text.Printf
import TypeMachine.TH.Internal.Utils
import TypeMachine.Type (fields, reifyType)
-- | Get the name of the 'Is' class generated for the given type
--
-- @
-- > isClassName ''User
-- IsUser
-- @
isClassName :: Name -> Name
isClassName = mkName . ("Is" ++) . capitalize . nameBase
-- | Get the name of the 'to' function generated for the given type
--
-- @
-- > toFuncName ''User
-- toUser
-- @
toFuncName :: Name -> Name
toFuncName = mkName . ("to" ++) . capitalize . nameBase
-- | Returns the declaration of the instance of 'Is' for a given type
--
-- @
-- > deriveIs ''Animal ''Dog
--
-- instance IsAnimal Dog where
-- ...
-- @
deriveIs :: Name -> Name -> Q [Dec]
deriveIs sourceTypeName destTypeName = do
destFields <- fields <$> reifyType destTypeName
sourceFields <- fields <$> reifyType sourceTypeName
let className = mkName ("Is" ++ nameBase sourceTypeName)
classFuncs <- fmap concat $ forM (zip [0 ..] $ Map.toList sourceFields) $ \(i, (n, (_, t))) ->
case Map.lookup n destFields of
Just _ -> do
getter <- fieldToGetter n
setter <- fieldToSetter (length destFields) i n
return [getter, setter]
Nothing ->
ifM
(fieldIsOptional t)
( do
getter <- fieldNameToMemptyFunDec n
setter <- fieldNameToNoopFunDec n
return [getter, setter]
)
( fail
( printf
"Type-Machine Error: Cannot define instance of %s for %s. Field '%s' is missing in %s "
(nameBase className)
destTypeStr
n
destTypeStr
)
)
let inlinePragmas = mkInlinePragmas $ Map.keys sourceFields
instanceDec =
InstanceD
Nothing
[]
(AppT (ConT className) (ConT destTypeName))
(inlinePragmas ++ classFuncs)
return [instanceDec]
where
destTypeStr = nameBase destTypeName
fieldNameToMemptyFunDec n =
funD (mkName $ fieldNameToIsGetter n) [clause [] (normalB [|const mzero|]) []]
fieldNameToNoopFunDec n =
funD
(mkName $ fieldNameToIsSetter n)
[clause [wildP, varP inputObjName] (normalB $ varE inputObjName) []]
where
inputObjName = mkName "x"
fieldToGetter n = do
let funName = mkName $ fieldNameToIsGetter n
resName = mkName "res"
expr = [|$(varE resName)|]
-- Note: using destTypeName makes Q think that we use the type, not the constructor
funD
funName
[clause [return $ RecP (mkName destTypeStr) [(mkName n, VarP resName)]] (normalB expr) []]
fieldToSetter fieldCount fieldPos fieldName = do
fieldsNames <-
forM
[0 .. (fieldCount - 1)]
( \i ->
if i == fieldPos
then return $ mkName "_"
else newName $ "f" ++ show i
)
let funName = mkName $ fieldNameToIsSetter fieldName
let newValueName = mkName "new"
let patt = ConP (mkName destTypeStr) [] (VarP <$> fieldsNames)
let body =
foldl
( \res f ->
res `AppE` case nameBase f of
"_" -> VarE newValueName
_ -> VarE f
)
(ConE $ mkName destTypeStr)
fieldsNames
funD funName [clause [varP newValueName, return $ patt] (normalB $ return body) []]
-- TODO handle non-parametric monadplus-es
-- Returns true is field is instance of Monad plus
fieldIsOptional :: TH.Type -> Q Bool
fieldIsOptional (AppT t _) = isInstance ''MonadPlus [t]
fieldIsOptional _ = return False
ifM mbool t f = do bool <- mbool; if bool then t else f
mkInlinePragmas :: [String] -> [Dec]
mkInlinePragmas fieldNames = mkInlinePragma . mkName <$> setterAndGetterNames
where
setterAndGetterNames =
foldr
(\field rest -> fieldNameToIsSetter field : fieldNameToIsGetter field : rest)
[]
fieldNames
mkInlinePragma fName = PragmaD $ InlineP fName Inline FunLike AllPhases
----- Definition
-- | Define the 'Is' class for the given type and generate the 'To' function
--
-- @
-- > data User = User { id :: Int, name :: String }
-- > defineIs ''User
--
-- class IsUser a where
-- getId :: a -> Int
-- getName :: a -> String
--
-- setId :: Int -> a -> a
-- setName :: String -> a -> a
--
-- toUser :: (IsUser a) => a -> User
-- toUser a = User (getId a) (getName a)
--
-- instance IsUser User where
-- ...
--
-- @
defineIs :: Name -> Q [Dec]
defineIs tyName = do
ty <- reifyType tyName
classTypeVar <- newName "a"
getters <- mapM (vbtToGetter classTypeVar) (Map.toList $ fields ty)
setters <- mapM (vbtToSetter classTypeVar) (Map.toList $ fields ty)
to <- defineTo tyName classTypeVar
isItself <- deriveIs tyName tyName
return $
ClassD
[]
(isClassName tyName)
[PlainTV classTypeVar BndrReq]
[]
(getters ++ setters ++ to)
: isItself
where
-- vbtToGetter a id Int == getId :: a -> Int
vbtToGetter :: Name -> (String, BangType) -> Q Dec
vbtToGetter classtypeVar (n, (_, t)) =
let
memberName = mkName $ fieldNameToIsGetter n
in
sigD memberName [t|$(varT classtypeVar) -> $(return t)|]
-- vbtToSetter a id Int == setId :: Int -> a -> a
vbtToSetter :: Name -> (String, BangType) -> Q Dec
vbtToSetter classtypeVar (n, (_, t)) =
let
memberName = mkName $ fieldNameToIsSetter n
in
sigD memberName [t|$(return t) -> $(varT classtypeVar) -> $(varT classtypeVar)|]
-- | Generate the 'To' function
--
-- @
-- > data User = User { id :: Int, name :: String }
--
-- toUser :: (IsUser a) => a -> User
-- toUser from = User (getId from) (getName from)
-- @
defineTo :: Name -> Name -> Q [Dec]
defineTo tyName tyVarName = do
ty <- reifyType tyName
toFuncType <-
sigD
toName
[t|$(varT tyVarName) -> $(conT tyName)|]
toFuncBody <-
let
from = mkName "from"
app =
foldl'
(\r n -> [|$r ($(varE $ mkName $ fieldNameToIsGetter n) $(varE from))|])
(conE $ mkName $ nameBase tyName)
(Map.keys $ fields ty)
in
funD (toFuncName tyName) [clause [varP from] (normalB app) []]
return [toFuncType, toFuncBody]
where
toName = toFuncName tyName
-- Internal
fieldNameToIsGetter :: String -> String
fieldNameToIsGetter = ("get" ++) . capitalize
fieldNameToIsSetter :: String -> String
fieldNameToIsSetter = ("set" ++) . capitalize