packages feed

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