packages feed

fused-effects-th-0.1.0.3: src/Control/Effect/TH.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}

-- | Defines splices that cut down on boilerplate associated with declaring new effects.
module Control.Effect.TH
  ( makeSmartConstructors,
  )
where

import Control.Algebra
import Control.Monad (join)
import Data.Char (toLower)
import Data.Foldable
import Data.Monoid (Ap (..))
import Data.Traversable
import Language.Haskell.TH (appT, arrowT, mkName, varT)
import qualified Language.Haskell.TH as TH

data PerEffect = PerEffect
  { effectType :: TH.TypeQ,
    effectTyVarCount :: Int,
    forallConstructor :: TH.Con
  }

-- Hideous hacks to deal with kindedness changes in newer TH versions.
#if MIN_VERSION_template_haskell(2,17,0)
type TyVarBinder = TH.TyVarBndrSpec

makeTV :: TH.Name -> TyVarBinder
makeTV n = TH.PlainTV n TH.inferredSpec

tvName :: TyVarBinder -> TH.Name
tvName = \case
  TH.PlainTV n _ -> n
  TH.KindedTV n _ _ -> n
#else
type TyVarBinder = TH.TyVarBndr

makeTV :: TH.Name -> TyVarBinder
makeTV = TH.plainTV

tvName :: TyVarBinder -> TH.Name
tvName = \case
  TH.PlainTV n -> n
  TH.KindedTV n _ -> n
#endif


data PerDecl = PerDecl
  { ctorArgs :: [TH.TypeQ],
    ctorConstraints :: [TH.TypeQ],
    ctorName :: TH.Name,
    ctorTyVars :: [TyVarBinder],
    functionName :: TH.Name,
    gadtReturnType :: TH.TypeQ,
    perEffect :: PerEffect
  }

-- | Given an effect type, this splice generates functions that create per-constructor request functions.
--
-- That is to say, given the standard @State@ type
--
-- @
--   data State s m k where
--     Get :: State s m s
--     Put :: s -> State s m ()
-- @
--
-- an invocation of @makeSmartConstructors ''State@ will generate code that looks like
--
--
-- >   get ::
-- >     forall (s :: Type) sig (m :: Type -> Type).
-- >     Has (State s) sig m =>
-- >     m s
-- >   get = send Get
-- >   {-# INLINEABLE get #-}
-- >    put ::
-- >     forall (s :: Type) sig (m :: Type -> Type).
-- >     Has (State s) sig m =>
-- >     s ->
-- >     m ()
-- >   put a = send (Put a)
-- >   {-# INLINEABLE put #-}
--
--
-- The type variables in each declared function signature will appear in the order
-- they were defined in the effect type.
makeSmartConstructors :: TH.Name -> TH.DecsQ
makeSmartConstructors typ =
  -- Lookup the provided type name.
  TH.reify typ >>= \case
    -- If it's a type constructor, record its type name.
    TH.TyConI (TH.DataD _ctx tn tvs _kind constructors _derive) ->
      let perEffect = PerEffect (TH.conT tn) (length tvs)
       in getAp (foldMap (Ap . makeDeclaration . perEffect) constructors)
    -- Die otherwise.
    other ->
      fail ("Can't generate definitions for a non-data-constructor: " <> TH.pprint other)

makeDeclaration :: PerEffect -> TH.DecsQ
makeDeclaration perEffect@PerEffect {..} = do
  -- Start by extracting the relevant parts of this particular constructor.
  (names, ctorArgs, constraints, returnType, ctorTyVars) <- case forallConstructor of
    TH.ForallC vars ctx (TH.GadtC names bangtypes (TH.AppT _ final)) ->
      pure (names, fmap snd bangtypes, ctx, final, vars)
    _ ->
      fail ("BUG: expected forall-qualified constructor, but didn't get one")
  -- Then iterate over the names of the constructors, emitting an injected
  -- method per name.
  fmap join . for names $ \ctorName -> do
    let downcase (x : xs) = mkName (toLower x : xs)
        downcase [] = error "attempted to downcase empty name"
        decl =
          PerDecl
            { ctorName = ctorName,
              functionName = downcase . TH.nameBase $ ctorName,
              ctorArgs = fmap pure ctorArgs,
              gadtReturnType = pure returnType,
              perEffect = perEffect,
              ctorTyVars = ctorTyVars,
              ctorConstraints = fmap pure constraints
            }
    sign <- makeSignature decl
    func <- makeFunction decl
    prag <- makePragma decl
    pure [sign, func, prag]

makePragma :: PerDecl -> TH.DecQ
makePragma PerDecl {..} =
  TH.pragInlD functionName TH.Inlinable TH.FunLike TH.AllPhases

makeFunction :: PerDecl -> TH.DecQ
makeFunction d =
  TH.funD (functionName d) [makeClause d]

makeClause :: PerDecl -> TH.ClauseQ
makeClause PerDecl {..} = TH.clause pats body []
  where
    body = TH.normalB [e|send ($(applies))|]
    pats = fmap TH.varP names
    -- Glue together the parameter to 'send', fully applied
    applies = foldl' (\e n -> e `TH.appE` TH.varE n) (TH.conE ctorName) names
    -- A source of a, b, c... names for function parameters.
    names = fmap (mkName . pure) (take (length ctorArgs) ['a' .. 'z'])

makeSignature :: PerDecl -> TH.DecQ
makeSignature PerDecl {perEffect = PerEffect {..}, ..} =
  let sigVar = mkName "sig"
      (rest, monadTV) = (init ctorTyVars, last ctorTyVars)
      getTyVar = varT . tvName
      monadName = getTyVar monadTV
      -- Build the parameter to Has by consulting the number of required type parameters.
      invocation = foldl' appT effectType (fmap getTyVar (take (effectTyVarCount - 2) rest))
      hasConstraint = [t|Has ($(invocation)) $(varT sigVar) $(monadName)|]
      -- Build the type signature by folding with (->) over the function arguments as needed.
      foldedSig = foldr (\a b -> arrowT `appT` a `appT` b) (monadName `appT` gadtReturnType) ctorArgs
      -- Glue together the Has and the per-constructor constraints.
      allConstraints = TH.cxt (hasConstraint : ctorConstraints)
   in TH.sigD functionName (TH.forallT (rest ++ [monadTV, makeTV sigVar]) allConstraints foldedSig)