recollections-0.1.1.0: src/Data/Recollections/TH.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
module Data.Recollections.TH
( -- * Collections generator
mkCollection
, mkIndices
-- * Distributive
-- $distributive
, mkDistributive
, mkDistributiveFor
, mkDistribute
-- * Representable
-- $representable
, mkRepresentable
, mkRepresentableFor
, mkIndex
, mkTabulate
-- * Tag instances
-- $tagInstances
, mkBounded
, mkEnum
) where
import Data.Char
import Data.Foldable
import Data.Traversable
import GHC.Enum (boundedEnumFrom, boundedEnumFromThen)
import GHC.Generics (Generic, Generic1, Generically1)
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (mkNameG_tc, mkNameG_v)
{- | Generate a @Collection a@ type from a enum-like type.
Every constructor is represented by a field.
Reserved words like @type@ get a @'@ suffix.'
> data Things = This | That
> deriving (Eq, Ord, Show, Enum, Bounded)
>
> mkCollection ''Things
>
> -- resulting splice
> data Collection a = Collection { this, that :: a }
> deriving (Eq, Show, Generic, Generic1, Functor, Foldable, Traversable)
> deriving Applicative via (Generically1 Collection)
A constructor may wrap the tag type of another collection, generated in the module that defines that tag.
The field then holds that whole collection:
> import Some qualified
>
> data Tag = Something Some.Tag | Local
>
> mkCollection ''Tag
>
> -- resulting splice
> data Collection a = Collection { something :: Some.Collection a, local :: a }
Every other collection generator applied to a nested tag requires the same generator in the nested module.
The instance generators call the nested instances instead, so the nested module needs the same instance generators.
-}
mkCollection :: Name -> Q [Dec]
mkCollection tags = do
let nothingsBanger = bang noSourceUnpackedness noSourceStrictness
names <- tagNames tags
fields <- for names \tag -> do
fieldType <- case tag of
Leaf _ -> pure $ varT (mkName "a")
Nested _ sub -> do
subCollection <- siblingOf mkNameG_tc "Collection" sub
pure $ conT subCollection `appT` varT (mkName "a")
pure $ varBangType (mkName . fieldNameOf $ tagName tag) $ bangType nothingsBanger fieldType
let constr = recC collectionName fields
let
derivs =
[ derivClause (Just StockStrategy)
[ conT ''Show
, conT ''Eq
, conT ''Generic
, conT ''Generic1
, conT ''Functor
, conT ''Foldable
, conT ''Traversable
]
, derivClause (Just $ ViaStrategy $ ConT ''Generically1 `AppT` ConT collectionName)
[ conT ''Applicative
]
]
pure <$> dataD mempty collectionName [collectionTyVar] Nothing [constr] derivs
-- | The @a@ binder of @data Collection a@.
--
-- @template-haskell-2.21@ (GHC 9.8) changed the binder flag of 'dataD'
-- from @()@ to 'BndrVis'.
#if MIN_VERSION_template_haskell(2,21,0)
collectionTyVar :: TyVarBndrVis
collectionTyVar = PlainTV (mkName "a") BndrReq
#else
collectionTyVar :: TyVarBndrUnit
collectionTyVar = PlainTV (mkName "a") ()
#endif
{- | Generate a value filled with the indices matching the fields.
> indices :: Collection Things
> indices = Collection { this = This, that = That }
This is useful with the Applicative instance to provide indexed operations:
> indexed c :: Collection a -> Collection (Things, a)
> indexed c = (,) <$> indices <*> c
-}
mkIndices :: Name -> Q [Dec]
mkIndices tags = do
let indicesName = mkName "indices"
names <- tagNames tags
sig <- sigD indicesName $ conT collectionName `appT` conT tags
fields <- for names \case
Leaf name -> pure . conE $ mkName name
Nested name sub -> do
subIndices <- siblingOf mkNameG_v "indices" sub
pure $ appE (appE (varE 'fmap) (conE $ mkName name)) (varE subIndices)
let body = foldl' appE (conE collectionName) fields
fun <- funD indicesName [ clause [] (normalB body) [] ]
pure [sig, fun]
-- * Distributive
{- $distributive
<https://hackage-content.haskell.org/package/distributive-0.6.3/docs/Data-Distributive.html>
The instance generators need the @distributive@ package as a dependency of the splicing package,
not of @recollections@.
The class is looked up in the splicing module scope, so it has to be imported there.
-}
{- | Generate a Distributive instance.
The splicing module must import "Data.Distributive", unqualified or qualified without an alias.
Use 'mkDistributiveFor' when the module is imported under an alias.
> instance Distributive Collection where
> distribute f = Collection
> { this = this <$> f
> , that = that <$> f
> }
-}
mkDistributive :: Name -> Q [Dec]
mkDistributive tags = do
distributive <- lookupClass "Data.Distributive" "Distributive"
mkDistributiveFor distributive tags
{- | Generate a Distributive instance for the explicitly provided class name.
> import qualified Data.Distributive as D
>
> mkDistributiveFor ''D.Distributive ''Things
-}
mkDistributiveFor :: Name -> Name -> Q [Dec]
mkDistributiveFor distributive tags = do
names <- tagNames tags
distributeName <- classMember mkNameG_v distributive "distribute"
pure <$> instanceD (cxt []) (conT distributive `appT` conT collectionName)
[ funD distributeName [distributeClause (const $ pure distributeName) names]
, inlineP distributeName
]
{- | Generate a dual of sequenceA.
Doesn't need the @distributive@ package, but clashes with the "Data.Distributive" import.
> distribute :: Functor f => f (Collection a) -> Collection (f a)
> distribute f = Collection
> { this = this <$> f
> , that = that <$> f
> , something = something <$> f
> , else' = else' <$> f
> , entirely = entirely <$> f
> }
-}
mkDistribute :: Name -> Q [Dec]
mkDistribute tags = do
let distributeName = mkName "distribute"
names <- tagNames tags
let f = mkName "f"
let a = mkName "a"
sig <- sigD distributeName $
forallT [] (cxt [conT ''Functor `appT` varT f]) $
appT (appT arrowT (varT f `appT` (conT collectionName `appT` (varT a)))) $
(conT collectionName `appT` (varT f `appT` varT a))
fun <- funD distributeName [distributeClause (siblingOf mkNameG_v "distribute") names]
inl <- inlineP distributeName
pure [sig, fun, inl]
distributeClause :: (Name -> Q Name) -> [Tag] -> Q Clause
distributeClause nested names = do
{- The binder must not shadow any field selector the body refers to by
'mkName' (those are resolved lexically, by occurrence name). Field names
always start with a lowercase letter, so a leading underscore is safe;
plain @f@ would break for a tag named @F@. -}
arg <- newName "_f"
fields <- for names \tag -> do
let projected = appE (varE 'fmap) (varE . mkName . fieldNameOf $ tagName tag) `appE` varE arg
case tag of
Leaf _ -> pure projected
Nested _ sub -> do
subDistribute <- nested sub
pure $ appE (varE subDistribute) projected
let body = foldl' appE (conE collectionName) fields
clause [varP arg] (normalB body) []
-- * Representable
{- $representable
<https://hackage-content.haskell.org/package/adjunctions-4.4.4/docs/Data-Functor-Rep.html>
The instance generators need the @adjunctions@ package as a dependency of the splicing package,
not of @recollections@.
The class is looked up in the splicing module scope, so it has to be imported there.
The splicing module also needs @TypeFamilies@ for the @Rep@ instance.
-}
{- | Generate a Representable instance.
The splicing module must import "Data.Functor.Rep", unqualified or qualified without an alias.
Use 'mkRepresentableFor' when the module is imported under an alias.
Representable requires a Distributive instance, see 'mkDistributive'.
> instance Representable Collection where
> type Rep Collection = Things
> index c = \case This -> this c; That -> that c
> tabulate k = Collection { this = k This, that = k That }
-}
mkRepresentable :: Name -> Q [Dec]
mkRepresentable tags = do
representable <- lookupClass "Data.Functor.Rep" "Representable"
mkRepresentableFor representable tags
{- | Generate a Representable instance for the explicitly provided class name.
> import qualified Data.Functor.Rep as R
>
> mkRepresentableFor ''R.Representable ''Things
-}
mkRepresentableFor :: Name -> Name -> Q [Dec]
mkRepresentableFor representable tags = do
names <- tagNames tags
repName <- classMember mkNameG_tc representable "Rep"
indexName <- classMember mkNameG_v representable "index"
tabulateName <- classMember mkNameG_v representable "tabulate"
pure <$> instanceD (cxt []) (conT representable `appT` conT collectionName)
[ tySynInstD $ tySynEqn Nothing (conT repName `appT` conT collectionName) (conT tags)
, funD indexName [indexClause (const $ pure indexName) names]
, inlineP indexName
, funD tabulateName [tabulateClause (const $ pure tabulateName) names]
, inlineP tabulateName
]
{- | Read a collection field using an index value.
Doesn't need the @adjunctions@ package, but clashes with the "Data.Functor.Rep" import.
> index :: Collection a -> Things -> a
> index c = \case This -> this c; That -> that c; …
-}
mkIndex :: Name -> Q [Dec]
mkIndex tags = do
let indexName = mkName "index"
names <- tagNames tags
let a = mkName "a"
sig <- sigD indexName $
appT (appT arrowT (conT collectionName `appT` varT a)) $
appT (appT arrowT (conT tags)) (varT a)
fun <- funD indexName [indexClause (siblingOf mkNameG_v "index") names]
inl <- inlineP indexName
pure [sig, fun, inl]
indexClause :: (Name -> Q Name) -> [Tag] -> Q Clause
indexClause nested names = do
bounds <- for names \tag ->
(,) tag <$> newName (fieldNameOf $ tagName tag)
matches <- for bounds \case
(Leaf name, bound) ->
pure $ match (conP (mkName name) []) (normalB $ varE bound) []
(Nested name sub, bound) -> do
subIndex <- nested sub
subTag <- newName "t"
pure $ match
(conP (mkName name) [varP subTag])
(normalB $ varE subIndex `appE` varE bound `appE` varE subTag)
[]
let body = lamCaseE matches
clause
[ conP collectionName $ map (varP . snd) bounds
]
(normalB body)
[]
{- | Generate a collection with a function from its indices
Doesn't need the @adjunctions@ package, but clashes with the "Data.Functor.Rep" import.
> tabulate :: (Things -> a) -> Collection a
> tabulate k = Collection { this = k This, that = k That }
-}
mkTabulate :: Name -> Q [Dec]
mkTabulate tags = do
let tabulateName = mkName "tabulate"
names <- tagNames tags
let a = mkName "a"
sig <- sigD tabulateName $
appT (appT arrowT (appT (appT arrowT (conT tags)) (varT a))) $
conT collectionName `appT` varT a
fun <- funD tabulateName [tabulateClause (siblingOf mkNameG_v "tabulate") names]
inl <- inlineP tabulateName
pure [sig, fun, inl]
tabulateClause :: (Name -> Q Name) -> [Tag] -> Q Clause
tabulateClause nested names = do
f <- newName "_f"
fields <- for names \case
Leaf name -> pure $ appE (varE f) $ conE (mkName name)
Nested name sub -> do
subTabulate <- nested sub
pure $ appE (varE subTabulate) $ infixE (Just $ varE f) (varE '(.)) (Just . conE $ mkName name)
let body = foldl' appE (conE collectionName) fields
clause [varP f] (normalB body) []
-- * Tag instances
{- $tagInstances
Stock 'Bounded' and 'Enum' only work for nullary constructors.
These generators also handle nested tags, using the 'Bounded' and 'Enum' instances of the nested tag types.
Those may be derived or generated.
-}
{- | Generate a 'Bounded' instance for the tag type.
> data Tag = Front Inner.Tag | Local | Back Inner.Tag
>
> instance Bounded Tag where
> minBound = Front minBound
> maxBound = Back maxBound
-}
mkBounded :: Name -> Q [Dec]
mkBounded tags = do
names <- tagNames tags
(first, final) <- case (names, reverse names) of
(first : _, final : _) -> pure (first, final)
_ -> fail $ "Can't bound a type without constructors: " <> show tags
let
bound method = \case
Leaf name -> conE $ mkName name
Nested name _ -> conE (mkName name) `appE` varE method
pure <$> instanceD (cxt []) (conT ''Bounded `appT` conT tags)
[ valD (varP 'minBound) (normalB $ bound 'minBound first) []
, valD (varP 'maxBound) (normalB $ bound 'maxBound final) []
]
{- | Generate an 'Enum' instance for the tag type, numbering the nested tags in place.
> data Tag = Front Inner.Tag | Local | Back Inner.Tag
>
> instance Enum Tag where
> fromEnum t = case t of
> Front x -> o0 + (fromEnum x - lo)
> Local -> o1
> Back x -> o2 + (fromEnum x - lo)
> where
> o0 = 0
> o1 = o0 + (hi - lo + 1)
> o2 = o1 + 1
> toEnum n
> | n < 0 = errorWithoutStackTrace …
> | n < o1 = Front (toEnum (n - o0 + lo))
> | n < o2 = Local
> | n < o3 = Back (toEnum (n - o2 + lo))
> | otherwise = errorWithoutStackTrace …
> where
> …
> o3 = o2 + (hi - lo + 1)
> enumFrom = boundedEnumFrom
> enumFromThen = boundedEnumFromThen
Here @lo@ and @hi@ stand for the inlined @fromEnum (minBound :: Inner.Tag)@ and @fromEnum (maxBound :: Inner.Tag)@.
The instance requires @Bounded Tag@, see 'mkBounded'.
-}
mkEnum :: Name -> Q [Dec]
mkEnum tags = do
names <- tagNames tags
fromEnumClause <- do
(offsets, bindings) <- runningOffsets . reverse . drop 1 $ reverse names
t <- newName "t"
matches <- fromEnumMatches $ zip names offsets
pure $ clause [varP t] (normalB $ caseE (varE t) matches) bindings
toEnumClause <- do
(offsets, bindings) <- runningOffsets names
n <- newName "n"
pure $ clause [varP n] (toEnumBody tags n $ zip3 names offsets (drop 1 offsets)) bindings
pure <$> instanceD (cxt []) (conT ''Enum `appT` conT tags)
[ funD 'fromEnum [fromEnumClause]
, funD 'toEnum [toEnumClause]
, valD (varP 'enumFrom) (normalB $ varE 'boundedEnumFrom) []
, valD (varP 'enumFromThen) (normalB $ varE 'boundedEnumFromThen) []
]
fromEnumMatches :: [(Tag, Name)] -> Q [Q Match]
fromEnumMatches spans =
for spans \case
(Leaf name, before) ->
pure $ match (conP (mkName name) []) (normalB $ varE before) []
(Nested name sub, before) -> do
subTag <- newName "t"
pure $ match
(conP (mkName name) [varP subTag])
(normalB $ plusE (varE before) (minusE (varE 'fromEnum `appE` varE subTag) (fromEnumOf 'minBound sub)))
[]
toEnumBody :: Name -> Name -> [(Tag, Name, Name)] -> Q Body
toEnumBody tags n spans =
guardedB $
[normalGE (lessThan . litE $ integerL 0) outOfRange]
<> map pick spans
<> [normalGE (varE 'otherwise) outOfRange]
where
lessThan bound = infixE (Just $ varE n) (varE '(<)) (Just bound)
pick = \case
(Leaf name, _, after) ->
normalGE (lessThan $ varE after) (conE $ mkName name)
(Nested name sub, before, after) ->
normalGE (lessThan $ varE after) $
conE (mkName name) `appE`
(varE 'toEnum `appE` plusE (minusE (varE n) (varE before)) (fromEnumOf 'minBound sub))
outOfRange =
varE 'errorWithoutStackTrace `appE`
infixE
(Just . stringE $ "toEnum{" <> nameBase tags <> "}: out of range: ")
(varE '(<>))
(Just $ varE 'show `appE` varE n)
runningOffsets :: [Tag] -> Q ([Name], [Q Dec])
runningOffsets names = do
first <- newName "o0"
rest <- for [1 .. length names] \i -> newName $ "o" <> show i
let offsets = first : rest
let
sizeE = \case
Leaf _ -> litE $ integerL 1
Nested _ sub -> plusE (minusE (fromEnumOf 'maxBound sub) (fromEnumOf 'minBound sub)) (litE $ integerL 1)
start = valD (varP first) (normalB . litE $ integerL 0) []
steps = do
(before, after, tag) <- zip3 offsets (drop 1 offsets) names
pure $ valD (varP after) (normalB $ plusE (varE before) (sizeE tag)) []
pure (offsets, start : steps)
fromEnumOf :: Name -> Name -> Q Exp
fromEnumOf bound sub = varE 'fromEnum `appE` sigE (varE bound) (conT sub)
plusE :: Q Exp -> Q Exp -> Q Exp
plusE a b = infixE (Just a) (varE '(+)) (Just b)
minusE :: Q Exp -> Q Exp -> Q Exp
minusE a b = infixE (Just a) (varE '(-)) (Just b)
-- * Utils
collectionName :: Name
collectionName = mkName "Collection"
lookupClass :: String -> String -> Q Name
lookupClass moduleName className = do
found <- traverse lookupTypeName [moduleName <> "." <> className, className]
case asum found of
Just name ->
pure name
Nothing ->
fail $ concat
[ className, " is not in scope. Import ", moduleName
, " or use mk", className, "For with an explicit class name."
]
classMember :: (String -> String -> String -> Name) -> Name -> String -> Q Name
classMember mkMember className member =
case (namePackage className, nameModule className) of
(Just package, Just moduleName) ->
pure $ mkMember package moduleName member
_ ->
fail $ "Expected an imported class name, got: " <> show className
data Tag
= Leaf String
| Nested String Name
tagName :: Tag -> String
tagName = \case
Leaf name -> name
Nested name _ -> name
tagNames :: Name -> Q [Tag]
tagNames tags =
reify tags >>= \case
TyConI (DataD _ _ _ _ constructors _) ->
foldrM (flip $ extractTags tags) [] constructors
_ ->
fail "Expected a type constructor name"
extractTags :: Name -> [Tag] -> Con -> Q [Tag]
extractTags tags acc = \case
NormalC name [] ->
pure $ Leaf (nameBase name) : acc
NormalC name [(_, ConT sub)]
| sub == tags ->
fail $ "A tag can't nest its own type: " <> show name
| otherwise ->
pure $ Nested (nameBase name) sub : acc
huh ->
-- Skipping would produce a 'Collection' with fewer fields than there are
-- tags, making the generated 'index' silently non-exhaustive.
fail $ "Expected a nullary constructor or one wrapping a nested tag type, got: " <> show huh
siblingOf :: (String -> String -> String -> Name) -> String -> Name -> Q Name
siblingOf mkNameG occ sub =
case (namePackage sub, nameModule sub) of
(Just pkg, Just modName) -> do
let sibling = mkNameG pkg modName occ
recover
(fail $ "Nested tag " <> show sub <> " needs " <> occ <> " generated in " <> modName)
(sibling <$ reify sibling)
_ ->
fail $ "Nested tag " <> show sub <> " must be a top-level type from another module"
-- | Lowercase the leading character and dodge reserved words.
fieldNameOf :: String -> String
fieldNameOf = \case
[] -> []
n : ame -> legalizeVariableName $ toLower n : ame
legalizeVariableName :: String -> String
legalizeVariableName name
| name `elem` illegal = name ++ "'"
| otherwise = name
-- | Haskell 2010 reserved words, which cannot be used as record field names.
illegal :: [String]
illegal =
[ "case", "class", "data", "default", "deriving", "do"
, "else", "foreign", "if", "import", "in"
, "infix", "infixl", "infixr", "instance"
, "let", "module", "newtype", "of"
, "then", "type", "where", "_"
]
inlineP :: Name -> Q Dec
inlineP name = pragInlD name Inline FunLike AllPhases