packages feed

fcf-family-0.2.0.3: src/Fcf/Family/TH.hs

{-# LANGUAGE
  CPP,
  ConstraintKinds,
  ImplicitParams,
  TemplateHaskell #-}

-- | Template Haskell script to promote a type family to first class.
module Fcf.Family.TH
  ( -- * Generate boilerplate
    fcfify
  , fcfifySkip
  , fcfify'

    -- * Using promoted families
  , promoteFamily
  , promoteNDFamily
  , familyName
  , applyFamily
  , consTuple
  , paramsProxy

    -- * Predicates
  , isTypeFamily
  , isTypeSynonym
  , isTypeFamilyOrSynonym
  ) where

import Control.Monad (when)
import Data.Function (on)
import Data.Functor (($>))
import Data.List (sort)
import Data.Maybe (fromMaybe)
import Data.Traversable (for)
import Data.Set (Set)
import qualified Data.Set as Set
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (getQ, putQ)

import Fcf.Core
import Fcf.Family hiding (Name)

-- | Generate the boilerplate needed to promote a type family to first class.
--
-- Required extensions:
--
-- - @DataKinds@
-- - @PolyKinds@
-- - @TypeFamilies@
--
-- If 'fcfify' is called more than once with the same 'Name' in the same module,
-- only the first invocation generates declarations; subsequent declarations
-- return the empty list, avoiding duplicate declarations in the current module.
--
-- For a stateless variant, use 'fcfify''.
--
-- See "Fcf.Family" for details on the encoding.
fcfify :: Name -> Q [Dec]
fcfify name = do
  check <- checkFcfified name
  if check then pure [] else fcfify' name

-- | Mark a type family as already fcifified.
fcfifySkip :: Name -> Q [Dec]
fcfifySkip name = checkFcfified name $> []

-- | Store invocations of 'fcfify' to avoid generating duplicate instances
-- in the current module (a minor performance optimization).
newtype Fcfified = Fcfified (Set Name)

-- | Check whether we've already seen this name.
-- Add the name to the registered set.
checkFcfified :: Name -> Q Bool
checkFcfified name = do
  Fcfified seen <- fromMaybe (Fcfified Set.empty) <$> getQ
  let check = name `Set.member` seen
  when (not check) (putQ (Fcfified (Set.insert name seen)))
  pure check


-- | Generate the boilerplate needed to promote a type family to first class.
--
-- Unlike 'fcfify', this always returns the same declarations for the same
-- named type.
fcfify' :: Name -> Q [Dec]
fcfify' name = reifyTyInfo name >>= fcfifyInfo
  where
    ?funName = "fcfify"
    ?name = name

-- | Get the quoted fcf 'Fcf.Core.Family.Name' of an existing type family.
familyName :: Name -> Type
familyName name = PromotedT 'MkName
  `AppT` lit (fromMaybe "" (namePackage name))
  `AppT` lit (fromMaybe "" (nameModule name))
  `AppT` lit (nameBase name)
  where lit = LitT . StrTyLit

-- | Promote a fcfified family, returning its partially applied 'Family' and
-- its arity. The result can be applied to a 'consTuple' of the appropriate size,
promoteFamily :: Name -> Q (Type, Int)
promoteFamily = promoteFamily_ ''Family

-- | Promote a fcfified family, returning its partially applied 'Family' and
-- its arity. The result can be applied to a 'consTuple' of the appropriate size,
promoteNDFamily :: Name -> Q (Type, Int)
promoteNDFamily = promoteFamily_ ''NDFamily

promoteFamily_ :: Name -> Name -> Q (Type, Int)
promoteFamily_ _Family name = do
  info <- reifyTyInfo name
  let arity = length (tiArgs info)
  pure (ConT _Family `AppT` tiNameT info `AppT` paramsProxy' info, arity)
  where
    ?funName = "promoteFamily_"

-- | Apply a promoted family.
--
-- If there are more arguments than the arity of the family (as returned by 'promoteFamily'),
-- they are split and applied properly:
-- the family's main arguments are collected in a 'consTuple' and
-- the rest are applied with 'AppT'.
--
-- If there are fewer arguments than the arity, the result is nonsense.
applyFamily :: Name -> [Q Type] -> Q Type
applyFamily name argsQ = do
  (fam, arity) <- promoteFamily name
  (args1, args2) <- splitAt arity <$> sequenceA argsQ
  pure (fam `AppT` consTuple args1 `appsT` args2)

paramsProxy :: Name -> Q Type
paramsProxy name = paramsProxy' <$> reifyTyInfo name
  where
    ?funName = "paramsProxy"

paramsProxy' :: TyInfo -> Type
paramsProxy' info = go (length (tiParams info))
  where
    go 0 = ConT ''P0
    go n = ConT ''PS `AppT` go (n-1)

reifyTyInfo :: (?funName :: String) => Name -> Q TyInfo
reifyTyInfo name = do
  let ?name = name
  info <- reify name
  case info of
    FamilyI dec _ -> reifyTyInfoDec dec
    TyConI dec -> reifyTyInfoDec dec
    _ -> errorNotType

-- | 'True' if it is a type family (open or closed).
isTypeFamily :: Name -> Q Bool
isTypeFamily name = isTypeFamilyInfo <$> reify name

-- | 'True' if it is a type synonym.
isTypeSynonym :: Name -> Q Bool
isTypeSynonym name = isTypeSynonymInfo <$> reify name

-- | 'True' if it is a type family or synonym.
isTypeFamilyOrSynonym :: Name -> Q Bool
isTypeFamilyOrSynonym name = liftA2 (||) isTypeFamilyInfo isTypeSynonymInfo <$> reify name

isTypeFamilyInfo :: Info -> Bool
isTypeFamilyInfo (FamilyI (OpenTypeFamilyD _) _) = True
isTypeFamilyInfo (FamilyI (ClosedTypeFamilyD _ _) _) = True
isTypeFamilyInfo _ = False

isTypeSynonymInfo :: Info -> Bool
isTypeSynonymInfo (TyConI (TySynD _ _ _)) = True
isTypeSynonymInfo _ = False

--

type ErrCtxt = (?funName :: String, ?name :: Name)

errorNotType :: ErrCtxt => Q a
errorNotType = fail (?funName ++ ": unexpected name, " ++ show ?name ++ " is not a type family or type synonym.")

-- Example:
--
-- @
-- -- Input
-- type F a b c = (...)
--
-- -- Output
-- type instance Params F 
-- @

reifyTyInfoDec :: ErrCtxt => Dec -> Q TyInfo
reifyTyInfoDec (TySynD name args _) = mkInfoHead name args StarT -- TODO: don't assume result kind is Type
reifyTyInfoDec (OpenTypeFamilyD t) = reifyTyInfoTFH t
reifyTyInfoDec (ClosedTypeFamilyD t _) = reifyTyInfoTFH t
reifyTyInfoDec _ = errorNotType

reifyTyInfoTFH :: ErrCtxt => TypeFamilyHead -> Q TyInfo
reifyTyInfoTFH (TypeFamilyHead name args resSig _) = do
  res <- getRes resSig
  mkInfoHead name args res

getRes :: ErrCtxt => FamilyResultSig -> Q Type
getRes NoSig = fail (?funName ++ ": implicit result type not supported")
getRes (KindSig k) = pure k
getRes (TyVarSig (KindedTV _ _ k)) = pure k
getRes (TyVarSig PlainTV{}) = fail (?funName ++ ": implicit result type not supported")

--

mkInfoHead :: Name -> [TyVarBndr a] -> Type -> Q TyInfo
mkInfoHead name args res = do
  args' <- for args (\arg -> case arg of
    PlainTV _ _ -> fail "unexpected unnanotated arguments"  -- as far as I understand, the binders given by reify are always annotated so this shouldn't happen
    KindedTV v _ k -> pure (v, k))
  let params = collectParams args' res
  pure (mkTyInfo name params args' res)

collectParams :: [(Name, Type)] -> Type -> [Name]
collectParams args res = collect Set.empty args where

  collect bound [] = snd (addVars bound [] (getVars res))  -- collect parameters from the result type
  collect bound ((v, k) : vs) =
    let (bound', ws) = addVars bound [] (getVars k) in
    ws ++ collect (Set.insert v bound') vs

  addVars bound ws [] = (bound, reverse ws)
  addVars bound ws (x : xs)
    | Set.member x bound = addVars bound ws xs
    | otherwise = addVars (Set.insert x bound) (x : ws) xs

data TyInfo = TyInfo
  { tiName :: Name
  , tiNameT :: Type     -- ^ Encoding of name as a 'Name'
  , tiParams :: [Name]
  , tiParamsT :: Type   -- ^ Params as a tuple
  , tiArgs :: [(Name, Type)]
  , tiArgsT :: Type
  , tiRes :: Type
  }

appsT :: Type -> [Type] -> Type
appsT = foldl' AppT

mkTyInfo :: Name -> [Name] -> [(Name, Type)] -> Type -> TyInfo
mkTyInfo name params args res = TyInfo
  { tiName = name
  , tiNameT = familyName name
  , tiParams = params
  , tiParamsT = consTuple (VarT <$> params)
  , tiArgs = args
  , tiArgsT = consTuple (uncurry (SigT . VarT) <$> args)
  , tiRes = res
  }

-- | Construct a tuple suitable for a 'Family' argument.
consTuple :: [Type] -> Type
consTuple = consTuple_ (PromotedTupleT 2) (PromotedTupleT 0)

consTupleT :: [Type] -> Type
consTupleT = consTuple_ (TupleT 2) (TupleT 0)

consTuple_ :: Type -> Type -> [Type] -> Type
consTuple_ _ unit [] = unit
consTuple_ tup unit (t : ts) = tup `AppT` t `AppT` consTuple_ tup unit ts

-- 

fcfifyInfo :: ErrCtxt => TyInfo -> Q [Dec]
fcfifyInfo info = do
  paramsD <- declareParams info
  argsD <- declareArgs info
  resD <- declareRes info
  familyD <- declareFamily info
  pure [paramsD, argsD, resD, familyD]

getVars :: Type -> [Name]
getVars (VarT v) = [v]
getVars (AppT t t') = getVars t ++ getVars t'
getVars (AppKindT t t') = getVars t ++ getVars t'
getVars (SigT t k) = getVars t ++ getVars k
getVars (InfixT t _ t') = getVars t ++ getVars t'
getVars (UInfixT t _ t') = getVars t ++ getVars t'
getVars (ParensT t) = getVars t
#if MIN_VERSION_template_haskell(2,19,0)
getVars (PromotedInfixT t _ t') = getVars t ++ getVars t'
getVars (PromotedUInfixT t _ t') = getVars t ++ getVars t'
#endif
getVars _ = []

declareParams :: TyInfo -> Q Dec
declareParams info = do
  let nParams = length (tiParams info)
  pure (TySynInstD (TySynEqn Nothing (ConT ''Params `AppT` tiNameT info) (consTupleT (replicate nParams StarT))))  -- TODO: don't guess Type for all params

declareArgs :: TyInfo -> Q Dec
declareArgs info = do
  pure (TySynInstD (TySynEqn Nothing
    (ConT ''Args_ `AppT` tiNameT info `AppT` tiParamsT info)
    (consTupleT (snd <$> tiArgs info))))
    
declareRes :: TyInfo -> Q Dec
declareRes info = do
  pure (TySynInstD (TySynEqn Nothing
    (ConT ''Res_ `AppT` tiNameT info `AppT` tiParamsT info `AppT` if isDT info then tiArgsT info else WildCardT)
    (tiRes info)))

isDT :: TyInfo -> Bool
isDT info = not (null (intersection (fst <$> tiArgs info) (getVars (tiRes info))))

intersection :: Ord a => [a] -> [a] -> [a]
intersection = intersectionSorted `on` sort

intersectionSorted :: Ord a => [a] -> [a] -> [a]
intersectionSorted [] _  = []
intersectionSorted _  [] = []
intersectionSorted xxs@(x : xs) yys@(y : ys) = case compare x y of
  EQ -> x : intersectionSorted xs ys
  LT -> intersectionSorted xs yys
  GT -> intersectionSorted xxs ys

declareFamily :: TyInfo -> Q Dec
declareFamily info = do
  pure (TySynInstD (TySynEqn Nothing
    (ConT ''Eval `AppT` (ConT ''Family_ `AppT` tiNameT info `AppT` SigT WildCardT (WildCardT `AppT` tiParamsT info) `AppT` tiArgsT info))
    (foldl' AppT (ConT (tiName info)) (VarT . fst <$> tiArgs info))))