packages feed

effectful-th-1.0.0.0: src/Effectful/TH.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
-- | Generate functions for performing operations of dynamically dispatched
-- effects via Template Haskell.
module Effectful.TH
  ( makeEffect
  , makeEffect_
  ) where

import Control.Monad
import Data.Char (toLower)
import Data.Foldable (foldl')
import Data.Maybe
import Language.Haskell.TH
import Language.Haskell.TH.Datatype
import Language.Haskell.TH.Datatype.TyVarBndr
import qualified Data.Map.Strict as Map

import Effectful
import Effectful.Dispatch.Dynamic

-- | For an effect data type @E@, @'makeEffect' E@ generates the appropriate
-- instance of 'DispatchOf' as well as functions for performing operations of
-- @E@ by 'send'ing them to the effect handler.
--
-- >>> :{
--   data E :: Effect where
--     Op1 :: Int -> m a -> E m a
--     Op2 :: IOE :> es => Int -> E (Eff es) ()
--     Op3 :: (forall r. m r -> m r) -> E m Int
--   makeEffect ''E
-- :}
--
-- >>> :kind! DispatchOf E
-- DispatchOf E :: Dispatch
-- = 'Dynamic
--
-- >>> :i op1
-- op1 :: (HasCallStack, E :> es) => Int -> Eff es a -> Eff es a
-- ...
--
-- >>> :i op2
-- op2 :: (HasCallStack, E :> es, IOE :> es) => Int -> Eff es ()
-- ...
--
-- >>> :i op3
-- op3 ::
--   (HasCallStack, E :> es) =>
--   (forall r. Eff es r -> Eff es r) -> Eff es Int
-- ...
--
-- The naming rule changes the first uppercase letter in the constructor name to
-- lowercase or removes the @:@ symbol in case of operators. Any fixity
-- annotations defined for the constructors are preserved for the corresponding
-- definitions.
makeEffect :: Name -> Q [Dec]
makeEffect = makeEffectImpl True

-- | Like 'makeEffect', but doesn't generate type signatures. This is useful
-- when you want to attach Haddock documentation to function signatures:
--
-- >>> :{
--   data Noop :: Effect where
--     Noop :: Noop m ()
--   makeEffect_ ''Noop
--   -- | Perform nothing at all.
--   noop :: Noop :> es => Eff es ()
-- :}
--
-- /Note:/ function signatures must be added /after/ the call to 'makeEffect_'.
makeEffect_ :: Name -> Q [Dec]
makeEffect_ = makeEffectImpl False

makeEffectImpl :: Bool -> Name -> Q [Dec]
makeEffectImpl makeSig effName = do
  checkRequiredExtensions
  info <- reifyDatatype effName
  dispatch <- do
    e <- getEff (ConT $ datatypeName info) (datatypeInstTypes info)
    let dispatchE = ConT ''DispatchOf `AppT` e
        dynamic   = PromotedT 'Dynamic
    pure . TySynInstD $ TySynEqn Nothing dispatchE dynamic
  ops <- traverse (makeCon makeSig) (constructorName <$> datatypeCons info)
  pure $ dispatch : concat (reverse ops)
  where
    getEff :: Type -> [Type] -> Q Type
    getEff e = \case
      [m, r]   -> do
        checkKind "the next to last" (ArrowT `AppT` StarT `AppT` StarT) m
        checkKind "the last" StarT r
        pure e
      (v : vs) -> getEff (e `AppT` forgetKind v) vs
      _        -> fail "The effect data type needs at least 2 type parameters"
      where
        forgetKind = \case
          SigT v _ -> v
          ty       -> ty

    checkKind which expected = \case
      SigT (VarT _) k
        | k == expected -> pure ()
        | otherwise -> fail
           $ "Expected " ++ which ++ " type parameter to have a kind "
          ++ pprint expected ++ ", got " ++ pprint k
      -- Weird type, let it through and see what happens.
      _ -> pure ()

-- | Generate a single definition of an effect operation.
makeCon :: Bool -> Name -> Q [Dec]
makeCon makeSig name = do
  fixity <- reifyFixity name
  typ <- reify name >>= \case
    DataConI _ typ _ -> pure typ
    _ -> fail $ "Not a data constructor: " ++ nameBase name

  (actionParams, (effTy, ename, resTy)) <- extractParams typ

  -- The 'ename' can be either:
  --
  -- - A variable for the monad, in which case we need to generate the @es@
  --   variable and substitute it later for 'Eff es'.
  --
  -- - A variable 'es' for the local 'Eff es' if the monad parameter was locally
  --   substituted in the contructor.
  --
  -- For example in the following effect:
  --
  -- data E :: Effect where
  --   E1 :: Int -> E m ()
  --   E2 :: IOE :> es => E (Eff es) ()
  --
  -- Processing 'E1' will yield 'Right m', but 'E2' will yield 'Left es'.
  --
  -- In the first case we need to substitute the variable ourselves in a few
  -- places, but in the second we're good since it was already substituted.
  (esName, maybeMonadName) <- case ename of
    Left  esName    -> pure (esName, Nothing)
    Right monadName -> (, Just monadName) <$> newName "es"

  let fnName = mkName . toSmartConName $ nameBase name
  fnArgs <- traverse (const $ newName "x") actionParams

  let esVar = VarT esName

      substM :: Type -> Type
      substM = case maybeMonadName of
        Just m  -> applySubstitution . Map.singleton m $ ConT ''Eff `AppT` esVar
        Nothing -> id

      (origActionVars, actionCtx) = extractCtx typ
      actionVars = case maybeMonadName of
        Just m  -> filter ((m /=) . tvName) origActionVars
                ++ [kindedTVSpecified esName $ ListT `AppT` ConT ''Effect]
        Nothing -> origActionVars

#if MIN_VERSION_template_haskell(2,17,0)
  -- In GHC >= 9.0 it's possible to generate the following body:
  --
  -- e x1 .. xN = send (E @ty1 .. @tyN x1 .. xN)
  --
  -- because specificities of constructor variables are exposed.
  --
  -- This allows to generate functions for such effects:
  --
  -- type family F ty :: Type
  -- data AmbEff :: Effect where
  --   AmbEff :: Int -> AmbEff m (F ty)
  --
  -- Sadly the version for GHC < 9 will not compile due to ambiguity error.
  let fnBody =
        let tvFlag = \case
              PlainTV  _ flag   -> flag
              KindedTV _ flag _ -> flag

            tyApps = (`mapMaybe` origActionVars) $ \v -> case tvFlag v of
              InferredSpec  -> Nothing
              SpecifiedSpec -> Just $ if maybeMonadName == Just (tvName v)
                                      then ConT ''Eff `AppT` esVar
                                      else VarT (tvName v)

            effCon = if makeSig
              then foldl' AppTypeE (ConE name) tyApps
              else                  ConE name
        in VarE 'send `AppE` foldl' (\f -> AppE f . VarE) effCon fnArgs
#else
  -- In GHC < 9.0, generate the following body:
  --
  -- e :: E v1 .. vN :> es => x1 -> .. -> xK -> E v1 .. vN (Eff es) r
  -- e x1 .. xK = send (E x1 .. xN :: E v1 .. vK (Eff es) r)
  let fnBody =
        let effOp  = foldl' (\f -> AppE f . VarE) (ConE name) fnArgs
            effSig = effTy `AppT` (ConT ''Eff `AppT` esVar) `AppT` substM resTy
        in if makeSig
           then VarE 'send `AppE` SigE effOp effSig
           else VarE 'send `AppE`      effOp
#endif
  let fnSig = ForallT actionVars
        (ConT ''HasCallStack : UInfixT effTy ''(:>) esVar : actionCtx)
        (makeTyp esVar substM resTy actionParams)

  let rest = FunD fnName [Clause (VarP <$> fnArgs) (NormalB fnBody) []]
           : maybeToList ((`InfixD` name) <$> fixity)
  (++ rest) <$> withHaddock name [SigD fnName fnSig | makeSig]

----------------------------------------
-- Helpers

toSmartConName :: String -> String
toSmartConName = \case
  (':' : xs) -> xs
  (x : xs)   -> toLower x : xs
  _          -> error "empty constructor name"

extractCtx :: Type -> ([TyVarBndrSpec], Cxt)
extractCtx = \case
  ForallT vars ctx _ -> (vars, ctx)
  ty                 -> error $ "unexpected type: " ++ show ty

extractParams :: Type -> Q ([Type], (Type, Either Name Name, Type))
extractParams = \case
  ForallT _ _ ty -> extractParams ty
  SigT ty _ -> extractParams ty
  ParensT ty -> extractParams ty
  ArrowT `AppT` a `AppT` ty -> do
    (args, ret) <- extractParams ty
    pure (a : args, ret)
#if MIN_VERSION_template_haskell(2,17,0)
  MulArrowT `AppT` _ `AppT` a `AppT` ty -> do
    (args, ret) <- extractParams ty
    pure (a : args, ret)
#endif
  effTy `AppT` monadTy `AppT` resTy -> case monadTy of
    VarT monadName -> pure ([], (effTy, Right monadName, resTy))
    ConT eff `AppT` VarT esName
      | eff == ''Eff -> pure ([], (effTy, Left esName, resTy))
    ty -> fail $ "Invalid instantiation of the monad parameter: " ++ pprint ty
  ty -> fail $ "Unexpected type: " ++ pprint ty

makeTyp :: Type -> (Type -> Type) -> Type -> [Type] -> Type
makeTyp esVar substM resTy = \case
  []       -> ConT ''Eff `AppT` esVar `AppT` substM resTy
  (p : ps) -> ArrowT `AppT` substM p `AppT` makeTyp esVar substM resTy ps

withHaddock :: Name -> [Dec] -> Q [Dec]
#if MIN_VERSION_template_haskell(2,18,0)
withHaddock name dec = withDecsDoc
  ("Perform the operation '" ++ nameBase name ++ "'.") (pure dec)
#else
withHaddock _ dec = pure dec
#endif

checkRequiredExtensions :: Q ()
checkRequiredExtensions = do
  missing <- filterM (fmap not . isExtEnabled) exts
  let ppMissing = map (\ext -> "{-# LANGUAGE " <> show ext <> " #-}") missing
  unless (null missing) . fail . unlines $
    [ "Generating functions requires additional language extensions.\n"
    , "You can enable them by adding them to the 'default-extensions'"
    , "field in the .cabal file or the following pragmas to the beginning"
    , "of the source file:\n"
    ] ++ ppMissing
  where
    exts = [ FlexibleContexts
           , ScopedTypeVariables
#if MIN_VERSION_template_haskell(2,17,0)
           , TypeApplications
#endif
           , TypeFamilies
           , TypeOperators
           ]