polysemy-mocks-0.1.0.0: src/Test/Polysemy/Mock/TH.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-}
module Test.Polysemy.Mock.TH (genMock) where
import Data.Bifunctor (first)
import Data.List (foldl')
import Language.Haskell.TH hiding (Strict)
import Polysemy (Embed, Members, Sem, interpret, pureT, reinterpretH)
import Polysemy.Internal (embed, send)
import Polysemy.Internal.TH.Common
import Polysemy.State (get, put)
import Test.Polysemy.Mock
-- | Generate mock using template-haskell.
-- Example usage:
--
-- > genMock ''Teletype
genMock :: Name -> Q [Dec]
genMock effName = do
(_, constructors) <- getEffectMetadata effName
-- MockImpl
let mockImplEffectType = ConT ''MockImpl `AppT` ConT effName `AppT` returnsEffect
let mockImplReturnType = mockImplEffectType `AppT` VarT (mkName "m")
let mockImplDataType = mockImplReturnType `AppT` VarT (mkName "a")
let mockImplConstructors =
map (mkMockConstructor mockImplReturnType) constructors
<> map (mkMockReturns mockImplReturnType) constructors
<> map (mkMockCalls mockImplReturnType) constructors
let mockImplD = DataInstD [] Nothing mockImplDataType Nothing mockImplConstructors []
-- MockState
mockStateConName <- newName (nameBase ''MockState <> nameBase effName)
let mockStateRec =
map mkMockStateCallsField constructors
<> map mkMockStateReturnsField constructors
let mockStateConstructor = RecC mockStateConName mockStateRec
let mockStateType = ConT ''MockState `AppT` ConT effName `AppT` returnsEffect
let mockStateD = DataInstD [] Nothing mockStateType Nothing [mockStateConstructor] []
-- initialMockState
let initialStateExps =
map mkInitialCalls constructors
<> map mkInitialReturns constructors
let initialStateBody = NormalB (RecConE mockStateConName initialStateExps)
let initialStateD = FunD 'initialMockState [Clause [] initialStateBody []]
-- mock
let mockMatches = map (mkMockMatch mockImplEffectType) constructors
let mockBody = NormalB (AppE (VarE 'interpret) (LamCaseE mockMatches))
let mockD = FunD 'mock [Clause [] mockBody []]
-- mockToState
let mockToStateMatches =
map (mkMockToStateMatch mockStateType) constructors
<> map (mkReturnsToStateMatch mockStateType) constructors
<> map (mkCallsToStateMatch mockStateType) constructors
let mockToStateBody = NormalB (AppE (VarE 'reinterpretH) (LamCaseE mockToStateMatches))
let mockToStateD = FunD 'mockToState [Clause [] mockToStateBody []]
-- instance
let mockInstanceD =
InstanceD
Nothing
[ConT ''Applicative `AppT` returnsEffect]
(ConT ''Mock `AppT` ConT effName `AppT` returnsEffect)
[ mockImplD,
mockStateD,
initialStateD,
mockD,
mockToStateD
]
-- makeSem
let semD =
concatMap (mkReturnsSem mockImplEffectType) constructors
<> concatMap (mkCallsSem mockImplEffectType) constructors
-- Bring it together
pure $ mockInstanceD : semD
mkMockConstructor :: Type -> ConLiftInfo -> Con
mkMockConstructor t c =
let args = (map (first (const defaultBang)) $ cliFunArgs c)
in GadtC [mockConName c] args (AppT t $ cliEffRes c)
mkMockReturns :: Type -> ConLiftInfo -> Con
mkMockReturns t c =
GadtC [returnsConName c] [(defaultBang, returnsFunctionType c)] (AppT t $ TupleT 0)
mkMockCalls :: Type -> ConLiftInfo -> Con
mkMockCalls t c =
GadtC [callsConName c] [] (AppT t (functionCallType c))
mkMockStateCallsField :: ConLiftInfo -> (Name, Bang, Type)
mkMockStateCallsField c =
(callsFieldName c, defaultBang, functionCallType c)
mkMockStateReturnsField :: ConLiftInfo -> (Name, Bang, Type)
mkMockStateReturnsField c =
(returnsFieldName c, defaultBang, returnsFunctionType c)
mkInitialCalls :: ConLiftInfo -> (Name, Exp)
mkInitialCalls c =
(callsFieldName c, ListE [])
mkInitialReturns :: ConLiftInfo -> (Name, Exp)
mkInitialReturns c =
let returnsFn =
case cliEffRes c of
(TupleT 0) -> LamE (map (const WildP) $ cliFunArgs c) $ AppE (VarE 'pure) (TupE [])
_ -> AppE (VarE 'error) (LitE (StringL $ "Unexpected mock invocation: " <> nameBase (cliFunName c)))
in (returnsFieldName c, returnsFn)
mkMockMatch :: Type -> ConLiftInfo -> Match
mkMockMatch t c =
let pat = ConP (cliConName c) (map (VarP . fst) (cliFunArgs c))
sendFn = VarE 'send
args = map (VarE . fst) (cliFunArgs c)
theMock = foldl' AppE (ConE $ mockConName c) args
body = NormalB (AppE (AppTypeE sendFn t) theMock)
in Match pat body []
mkMockToStateMatch :: Type -> ConLiftInfo -> Match
mkMockToStateMatch t c =
let pat = ConP (mockConName c) (map VarP vars)
--
vars = map fst (cliFunArgs c)
newArgs = if length (cliFunArgs c) == 1
then ListE [ VarE . fst . head . cliFunArgs $ c]
else
#if MIN_VERSION_template_haskell(2,16,0)
ListE [TupE (map (Just . VarE . fst) $ cliFunArgs c)]
#else
ListE [TupE (map (VarE . fst) $ cliFunArgs c)]
#endif
oldArgs = AppE (VarE (callsFieldName c)) (VarE stateName)
allArgs = InfixE (Just oldArgs) (VarE '(++)) (Just newArgs)
newState = RecUpdE (VarE stateName) [(callsFieldName c, allArgs)]
--
applyReturnsFn = foldl' AppE (VarE (returnsFieldName c)) (VarE stateName : map VarE vars)
embedReturnsFn = AppE (VarE 'embed) applyReturnsFn
returnAsPureT = NoBindS $ InfixE (Just (VarE 'pureT)) (VarE '(=<<)) (Just embedReturnsFn)
body =
NormalB
( DoE
[ getState t,
putState newState,
returnAsPureT
]
)
in Match pat body []
mkReturnsToStateMatch :: Type -> ConLiftInfo -> Match
mkReturnsToStateMatch t c =
let f = mkName "f"
pat = ConP (returnsConName c) [VarP f]
newState = RecUpdE (VarE stateName) [(returnsFieldName c, VarE f)]
returnNothing = NoBindS $ AppE (VarE 'pureT) (TupE [])
body =
NormalB
( DoE
[ getState t,
putState newState,
returnNothing
]
)
in Match pat body []
mkCallsToStateMatch :: Type -> ConLiftInfo -> Match
mkCallsToStateMatch t c =
let pat = ConP (callsConName c) []
returnCalls = NoBindS $ AppE (VarE 'pureT) (AppE (VarE (callsFieldName c)) (VarE stateName))
body =
NormalB
( DoE
[ getState t,
returnCalls
]
)
in Match pat body []
mkReturnsSem ::
-- | Should look like: @MockImpl Teletype n@
-- n is assumed to be 'stateEffectName', maybe this is problematic, but it works for now
Type ->
ConLiftInfo ->
[Dec]
mkReturnsSem mockImplEffType c =
let funcName = mkName ("mock" <> nameBase (cliConName c) <> "Returns")
body = NormalB (InfixE (Just $ VarE 'send) (VarE '(.)) (Just $ ConE (returnsConName c)))
appArrowT = AppT . AppT ArrowT
r = VarT $ mkName "r"
semr t = ConT ''Sem `AppT` r `AppT` t
typ = ForallT [] [membersEffListType mockImplEffType r] (returnsFunctionType c `appArrowT` semr (TupleT 0))
in [ SigD funcName typ,
FunD funcName [Clause [] body []]
]
mkCallsSem ::
-- | Should look like: @MockImpl Teletype n@
-- n is assumed to be 'stateEffectName', maybe this is problematic, but it works for now
Type ->
ConLiftInfo ->
[Dec]
mkCallsSem mockImplEffType c =
let funcName = mkName ("mock" <> nameBase (cliConName c) <> "Calls")
typeAppliedSend = VarE 'send `AppTypeE` mockImplEffType
body = NormalB $ typeAppliedSend `AppE` ConE (callsConName c)
r = VarT $ mkName "r"
semr t = ConT ''Sem `AppT` r `AppT` t
typ = ForallT [PlainTV returnsEffectName, PlainTV $ mkName "r"] [membersEffListType mockImplEffType r] (semr $ functionCallType c)
in [ SigD funcName typ,
FunD funcName [Clause [] body []]
]
membersEffListType :: Type -> Type -> Type
membersEffListType mockImplEffType r =
let embededStateEffect = ConT ''Embed `AppT` VarT returnsEffectName
appConsT = AppT . AppT PromotedConsT
effList = foldr appConsT PromotedNilT [mockImplEffType, embededStateEffect]
in ConT ''Members `AppT` effList `AppT` r
getState :: Type -> Stmt
getState t = BindS (VarP stateName) (VarE 'get `AppTypeE` t)
putState :: Exp -> Stmt
putState newState = NoBindS (AppE (VarE 'put) newState)
stateName :: Name
stateName = mkName "state"
callsConName :: ConLiftInfo -> Name
callsConName c = mkName ("Mock" <> nameBase (cliConName c) <> "Calls")
returnsConName :: ConLiftInfo -> Name
returnsConName c = mkName ("Mock" <> nameBase (cliConName c) <> "Returns")
mockConName :: ConLiftInfo -> Name
mockConName c = mkName ("Mock" <> nameBase (cliConName c))
callsFieldName :: ConLiftInfo -> Name
callsFieldName c = mkName (nameBase (cliFunName c) <> "Calls")
returnsFieldName :: ConLiftInfo -> Name
returnsFieldName c = mkName (nameBase (cliFunName c) <> "Returns")
defaultBang :: Bang
defaultBang = Bang NoSourceUnpackedness NoSourceStrictness
functionCallType :: ConLiftInfo -> Type
functionCallType c =
let arity = length $ cliFunArgs c
in if arity == 1
then AppT ListT $ snd $ head $ cliFunArgs c
else AppT ListT $ foldl' AppT (TupleT arity) (map snd $ cliFunArgs c)
returnsFunctionType :: ConLiftInfo -> Type
returnsFunctionType c =
let argTypes = (map snd $ cliFunArgs c)
returnType = (AppT returnsEffect $ cliEffRes c)
in foldr (AppT . AppT ArrowT) returnType argTypes
returnsEffect :: Type
returnsEffect = VarT returnsEffectName
returnsEffectName :: Name
returnsEffectName = mkName "n"