polysemy-0.6.0.0: src/Polysemy/Internal/TH/Common.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS_HADDOCK not-home #-}
module Polysemy.Internal.TH.Common
( ConLiftInfo (..)
, getEffectMetadata
, makeMemberConstraint
, makeMemberConstraint'
, makeSemType
, makeInterpreterType
, makeEffectType
, makeUnambiguousSend
, checkExtensions
, foldArrows
) where
import Control.Monad
import Data.Bifunctor
import Data.Char (toLower)
import Data.Either
import Data.Generics hiding (Fixity)
import Data.List
import qualified Data.Map.Strict as M
import Data.Tuple
import Language.Haskell.TH
import Language.Haskell.TH.Datatype
import Language.Haskell.TH.PprLib
import Polysemy.Internal (Sem, send)
import Polysemy.Internal.Union (MemberWithError)
#if __GLASGOW_HASKELL__ >= 804
import Prelude hiding ((<>))
#endif
------------------------------------------------------------------------------
-- | Given an effect name, eg @''State@, get information about the type
-- constructor, and about each of its data constructors.
getEffectMetadata :: Name -> Q (DatatypeInfo, [ConLiftInfo])
getEffectMetadata type_name = do
dt_info <- reifyDatatype type_name
cl_infos <- traverse (mkCLInfo dt_info) $ datatypeCons dt_info
pure (dt_info, cl_infos)
------------------------------------------------------------------------------
-- | Turn a 'ConLiftInfo' for @Foo@ into a @Member Foo r@ constraint.
makeMemberConstraint :: Name -> ConLiftInfo -> Pred
makeMemberConstraint r cli = makeMemberConstraint' r $ makeEffectType cli
------------------------------------------------------------------------------
-- | Given a 'ConLiftInfo', get the corresponding effect type.
makeEffectType :: ConLiftInfo -> Type
makeEffectType cli
= foldl' AppT (ConT $ cliEffName cli)
$ cliEffArgs cli
------------------------------------------------------------------------------
-- | @'makeMemberConstraint'' r type@ will produce a @Member type r@
-- constraint.
makeMemberConstraint' :: Name -> Type -> Pred
makeMemberConstraint' r eff = classPred ''MemberWithError [eff, VarT r]
------------------------------------------------------------------------------
-- | @'makeSemType' r a@ will produce a @'Polysemy.Sem' r a@ type.
makeSemType :: Name -> Type -> Type
makeSemType r result = ConT ''Sem `AppT` VarT r `AppT` result
------------------------------------------------------------------------------
-- | @'makeInterpreterType' con r a@ will produce a @'Polysemy.Sem' (Effect ':
-- r) a -> 'Polysemy.Sem' r a@ type, where @Effect@ is the effect corresponding
-- to the 'ConLiftInfo' for @con@.
makeInterpreterType :: ConLiftInfo -> Name -> Type -> Type
makeInterpreterType cli r result =
foldArrows (makeSemType r result)
$ pure
$ ConT ''Sem
`AppT` (PromotedConsT `AppT` makeEffectType cli `AppT` VarT r)
`AppT` result
------------------------------------------------------------------------------
-- | Given a 'ConLiftInfo', this will produce an action for it. It's arguments
-- will come from any variables in scope that correspond to the 'cliArgs' of
-- the 'ConLiftInfo'.
makeUnambiguousSend :: Bool -> ConLiftInfo -> Exp
makeUnambiguousSend should_mk_sigs cli =
let fun_args_names = fmap fst $ cliArgs cli
action = foldl1' AppE
$ ConE (cliConName cli) : (VarE <$> fun_args_names)
eff = foldl' AppT (ConT $ cliEffName cli) $ args
-- see NOTE(makeSem_)
args = (if should_mk_sigs then id else map capturableTVars)
$ cliEffArgs cli ++ [sem, cliResType cli]
sem = ConT ''Sem `AppT` VarT (cliUnionName cli)
in AppE (VarE 'send) $ SigE action eff
------------------------------------------------------------------------------
-- | Info about constructor being lifted; use 'mkCLInfo' to create one.
data ConLiftInfo = CLInfo
{ -- | Name of effect's type constructor
cliEffName :: Name
-- | Effect-specific type arguments
, cliEffArgs :: [Type]
-- | Result type specific to action
, cliResType :: Type
-- | Name of action constructor
, cliConName :: Name
-- | Name of final function
, cliFunName :: Name
-- | Fixity of function used as an operator
, cliFunFixity :: Maybe Fixity
-- | Final function arguments
, cliArgs :: [(Name, Type)]
-- | Constraints of final function
, cliFunCxt :: Cxt
-- | Name of type variable parameterizing 'Sem'
, cliUnionName :: Name
} deriving Show
------------------------------------------------------------------------------
-- | Creates info about smart constructor being created from info about action
-- and it's parent type.
mkCLInfo :: DatatypeInfo -> ConstructorInfo -> Q ConLiftInfo
mkCLInfo dti ci = do
let cliEffName = datatypeName dti
(raw_cli_eff_args, [m_arg, raw_cli_res_arg]) <-
case splitAtEnd 2 $ datatypeInstTypes dti of
r@(_, [_, _]) -> pure r
_ -> missingEffArgs cliEffName
m_name <-
case tVarName m_arg of
Just r -> pure r
Nothing -> mArgNotVar cliEffName m_arg
cliUnionName <- newName "r"
cliFunFixity <- reifyFixity $ constructorName ci
let normalizeType = replaceMArg m_name cliUnionName
. simplifyKinds
. applySubstitution eq_pairs
-- We extract equality constraints with variables to unify them
-- manually - this makes type errors more readable. Plus we replace
-- kind of result with 'Type' if it is a type variable.
(eq_pairs, cliFunCxt) = first (M.fromList . maybeResKindToType)
$ partitionEithers
$ eqPairOrCxt <$> constructorContext ci
maybeResKindToType = maybe id (\k ps -> (k, StarT) : ps)
$ tVarName $ tvKind $ last
$ datatypeVars dti
cliEffArgs = normalizeType <$> raw_cli_eff_args
cliResType = normalizeType raw_cli_res_arg
cliConName = constructorName ci
cliFunName = liftFunNameFromCon cliConName
arg_types = normalizeType <$> constructorFields ci
arg_names <- replicateM (length arg_types) $ newName "x"
pure CLInfo{cliArgs = zip arg_names arg_types, ..}
------------------------------------------------------------------------------
-- Error messages and checks
mArgNotVar :: Name -> Type -> Q a
mArgNotVar name mArg = fail $ show
$ text "Monad argument ‘" <> ppr mArg <> text "’ in effect ‘"
<> ppr name <> text "’ is not a type variable"
missingEffArgs :: Name -> Q a
missingEffArgs name = fail $ show
$ text "Effect ‘" <> ppr name
<> text "’ has not enough type arguments"
$+$ nest 4
( text "At least monad and result argument are required, e.g.:"
$+$ nest 4
( text ""
$+$ ppr (DataD [] base args Nothing [] []) <+> text "..."
$+$ text ""
)
)
where
base = capturableBase name
args = PlainTV . mkName <$> ["m", "a"]
------------------------------------------------------------------------------
-- | Fail the 'Q' monad whenever the given 'Extension's aren't enabled in the
-- current module.
checkExtensions :: [Extension] -> Q ()
checkExtensions exts = do
states <- zip exts <$> traverse isExtEnabled exts
maybe (pure ())
(\(ext, _) -> fail $ show
$ char '‘' <> text (show ext) <> char '’'
<+> text "extension needs to be enabled for Polysemy's Template Haskell to work")
(find (not . snd) states)
------------------------------------------------------------------------------
-- | Constructs capturable name from base of input name.
capturableBase :: Name -> Name
capturableBase = mkName . nameBase
------------------------------------------------------------------------------
-- | Replaces use of @m@ in type with @Sem r@.
replaceMArg :: TypeSubstitution t => Name -> Name -> t -> t
replaceMArg m r = applySubstitution $ M.singleton m $ ConT ''Sem `AppT` VarT r
------------------------------------------------------------------------------
-- Removes 'Type' and variable kind signatures from type.
simplifyKinds :: Type -> Type
simplifyKinds = everywhere $ mkT $ \case
SigT t StarT -> t
SigT t VarT{} -> t
ForallT bs cs t -> ForallT (goBndr <$> bs) (simplifyKinds <$> cs) t
where
goBndr (KindedTV n StarT ) = PlainTV n
goBndr (KindedTV n VarT{}) = PlainTV n
goBndr b = b
t -> t
------------------------------------------------------------------------------
-- | Converts equality constraint with type variable to name and type pair if
-- possible or leaves constraint as is.
eqPairOrCxt :: Pred -> Either (Name, Type) Pred
eqPairOrCxt p = case asEqualPred p of
Just (VarT n, b) -> Left (n, b)
Just (a, VarT n) -> Left (n, a)
_ -> Right p
------------------------------------------------------------------------------
-- | Creates name of lifting function from action name.
liftFunNameFromCon :: Name -> Name
liftFunNameFromCon n = mkName $
case nameBase n of
':' : cs -> cs
c : cs -> toLower c : cs
"" -> error "liftFunNameFromCon: empty constructor name"
------------------------------------------------------------------------------
-- | Folds a list of 'Type's into a right-associative arrow 'Type'.
foldArrows :: Type -> [Type] -> Type
foldArrows = foldr (AppT . AppT ArrowT)
------------------------------------------------------------------------------
-- | Extracts name from type variable (possibly nested in signature and/or
-- some context), returns 'Nothing' otherwise.
tVarName :: Type -> Maybe Name
tVarName = \case
ForallT _ _ t -> tVarName t
SigT t _ -> tVarName t
VarT n -> Just n
ParensT t -> tVarName t
_ -> Nothing
------------------------------------------------------------------------------
-- | 'splitAt' counting from the end.
splitAtEnd :: Int -> [a] -> ([a], [a])
splitAtEnd n = swap . join bimap reverse . splitAt n . reverse
------------------------------------------------------------------------------
-- | Converts names of all type variables in type to capturable ones based on
-- original name base. Use with caution, may create name conflicts!
capturableTVars :: Type -> Type
capturableTVars = everywhere $ mkT $ \case
VarT n -> VarT $ capturableBase n
ForallT bs cs t -> ForallT (goBndr <$> bs) (capturableTVars <$> cs) t
where
goBndr (PlainTV n ) = PlainTV $ capturableBase n
goBndr (KindedTV n k) = KindedTV (capturableBase n) $ capturableTVars k
t -> t