syntactic-0.7: Language/Syntactic/Syntax.hs
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Generic representation of typed syntax trees
--
-- As a simple demonstration, take the following simple language:
--
-- > data Expr1 a
-- > where
-- > Num1 :: Int -> Expr1 Int
-- > Add1 :: Expr1 Int -> Expr1 Int -> Expr1 Int
--
-- Using the present library, this can be rewritten as follows:
--
-- > data Num2 a where Num2 :: Int -> Num2 (Full Int)
-- > data Add2 a where Add2 :: Add2 (Int :-> Int :-> Full Int)
-- >
-- > type Expr2 a = ASTF (Num2 :+: Add2) a
--
-- Note that @Num2@ and @Add2@ are /non-recursive/. The only recursive data type
-- here is 'AST', which is provided by the library. Now, the important point is
-- that @Expr1@ and @Expr2@ are completely isomorphic! This is indicated by the
-- following conversions:
--
-- > conv12 :: Expr1 a -> Expr2 a
-- > conv12 (Num1 n) = inject (Num2 n)
-- > conv12 (Add1 a b) = inject Add2 :$: conv12 a :$: conv12 b
-- >
-- > conv21 :: Expr2 a -> Expr1 a
-- > conv21 (project -> Just (Num2 n)) = Num1 n
-- > conv21 ((project -> Just Add2) :$: a :$: b) = Add1 (conv21 a) (conv21 b)
--
-- A key property here is that the patterns in @conv21@ are actually complete.
--
-- So, why should one use @Expr2@ instead of @Expr1@? The answer is that @Expr2@
-- can be processed by generic algorithms defined over 'AST', for example:
--
-- > countNodes :: ASTF domain a -> Int
-- > countNodes = count
-- > where
-- > count :: AST domain a -> Int
-- > count (Symbol _) = 1
-- > count (a :$: b) = count a + count b
--
-- Furthermore, although @Expr2@ was defined to use exactly the constructors
-- 'Num2' and 'Add2', it is possible to leave the set of constructors open,
-- leading to more modular and reusable code. This can be seen by relaxing the
-- types of @conv12@ and @conv21@:
--
-- > conv12 :: (Num2 :<: dom, Add2 :<: dom) => Expr1 a -> ASTF dom a
-- > conv21 :: (Num2 :<: dom, Add2 :<: dom) => ASTF dom a -> Expr1 a
--
-- This way of encoding open data types is taken from /Data types à la carte/
-- (Wouter Swierstra, /Journal of Functional Programming/, 2008). However, we do
-- not need Swierstra's fixed-point machinery for recursive data types. Instead
-- we rely on 'AST' being recursive.
module Language.Syntactic.Syntax
( -- * Syntax trees
Full (..)
, (:->) (..)
, HList (..)
, WrapFull (..)
, ConsType
, ConsEval
, EvalResult
, ConsWit (..)
, WitnessCons (..)
, fromEval
, toEval
, listHList
, listHListM
, mapHList
, mapHListM
, appHList
, appEvalHList
, ($:)
, AST (..)
, ASTF
, (:+:) (..)
, ApplySym
, appSym
, appSymCtx
-- * Subsumption
, (:<:) (..)
, injCtx
, prjCtx
-- * Syntactic sugar
, Syntactic (..)
, resugar
, SyntacticN (..)
, sugarSym
, sugarSymCtx
-- * AST processing
, queryNode
, queryNodeSimple
, transformNode
-- * Restricted syntax trees
, Sat (..)
, Witness (PolyWit, SimpleWit)
-- TODO A warning reports that these are already exported by 'Sat (..)',
-- but that is actually not the case. This seems to have been fixed
-- recently:
--
-- http://hackage.haskell.org/trac/ghc/ticket/2436#comment:12
--
-- I don't know if the fix just removes the warning, or if it means
-- that 'Sat (..)' is enough.
, witnessByProxy
, SatWit (..)
, fromSatWit
, WitnessSat (..)
, MaybeWitnessSat (..)
, maybeWitnessSatDefault
, withContext
, Poly
, poly
, SimpleCtx
, simpleCtx
) where
import Control.Monad.Identity
import Data.Typeable
import Data.Proxy
--------------------------------------------------------------------------------
-- * Syntax trees
--------------------------------------------------------------------------------
-- | The type of a fully applied constructor
newtype Full a = Full { result :: a }
deriving (Eq, Show, Typeable)
-- | The type of a partially applied (or unapplied) constructor
newtype a :-> b = Partial (a -> b)
deriving (Typeable)
-- | Heterogeneous list, indexed by a container type and a 'ConsType'
data family HList (c :: * -> *) a
data instance HList c (Full a) = Nil
data instance HList c (a :-> b) = Typeable a => c (Full a) :*: HList c b
-- The 'Typeable' constraint is needed in order to be able to rebuild an 'AST'
-- from an 'HList' (since '(:$:)' has a `Typeable` constraint).
infixr :->, :*:
-- | Can be used to turn a type constructor indexed by @a@ to a type constructor
-- indexed by @(`Full` a)@. This is useful together with 'HList', which assumes
-- its constructor to be indexed by @(`Full` a)@. That is, use
--
-- > HList (WrapFull c) ...
--
-- instead of
--
-- > HList c ...
--
-- if @c@ is not indexed by @(`Full` a)@.
data WrapFull c a
where
WrapFull :: { unwrapFull :: c a } -> WrapFull c (Full a)
-- | Fully or partially applied constructor
--
-- This class is private to the module to guarantee that all members of the
-- class have the form:
--
-- > Full a
-- > a1 :-> Full a2
-- > a1 :-> a2 :-> ... :-> Full an
--
-- The closed class also has the property:
-- @ConsType' (a :-> b)@ iff. @ConsType' b@.
class ConsType' a
where
type ConsEval' a
type EvalResult' a
fromEval' :: ConsEval' a -> a
toEval' :: a -> ConsEval' a
listHList' :: (forall a . c (Full a) -> b) -> HList c a -> [b]
listHListM' :: Monad m => (forall a . c (Full a) -> m b) -> HList c a -> m [b]
mapHList' :: (forall a . c1 (Full a) -> c2 (Full a)) -> HList c1 a -> HList c2 a
mapHListM' :: Monad m => (forall a . c1 (Full a) -> m (c2 (Full a))) -> HList c1 a -> m (HList c2 a)
appHList' :: AST dom a -> HList (AST dom) a -> ASTF dom (EvalResult a)
appEvalHList' :: ConsEval a -> HList Identity a -> EvalResult a
instance ConsType' (Full a)
where
type ConsEval' (Full a) = a
type EvalResult' (Full a) = a
fromEval' = Full
toEval' = result
listHList' f Nil = []
listHListM' f Nil = return []
mapHList' f Nil = Nil
mapHListM' f Nil = return Nil
appHList' a Nil = a
appEvalHList' a Nil = a
instance ConsType' b => ConsType' (a :-> b)
where
type ConsEval' (a :-> b) = a -> ConsEval' b
type EvalResult' (a :-> b) = EvalResult' b
fromEval' = Partial . (fromEval' .)
toEval' (Partial f) = toEval' . f
listHList' f (a :*: as) = f a : listHList' f as
listHListM' f (a :*: as) = sequence (f a : listHList' f as)
mapHList' f (a :*: as) = f a :*: mapHList' f as
mapHListM' f (a :*: as) = liftM2 (:*:) (f a) (mapHListM' f as)
appHList' c (a :*: as) = appHList' (c :$: a) as
appEvalHList' f (a :*: as) = appEvalHList' (f $ result $ runIdentity a) as
-- | Fully or partially applied constructor
--
-- This is a public alias for the hidden class 'ConsType''. The only instances
-- are:
--
-- > instance ConsType' (Full a)
-- > instance ConsType' b => ConsType' (a :-> b)
class ConsType' a => ConsType a
instance ConsType' a => ConsType a
-- | Maps a 'ConsType' to a simpler form where ':->' has been replaced by @->@,
-- and 'Full' has been removed. This is a public alias for the hidden type
-- 'ConsEval''.
type ConsEval a = ConsEval' a
-- | Returns the result type ('Full' removed) of a 'ConsType'. This is a public
-- alias for the hidden type 'EvalResult''.
type EvalResult a = EvalResult' a
-- | A witness of @(`ConsType` a)@
data ConsWit a
where
ConsWit :: ConsType a => ConsWit a
-- | Expressions in syntactic are supposed to have the form
-- @(`ConsType` a => expr a)@. This class lets us witness the 'ConsType'
-- constraint of an expression without examining the expression.
class WitnessCons expr
where
witnessCons :: expr a -> ConsWit a
-- | Make a constructor evaluation from a 'ConsEval' representation
fromEval :: ConsType a => ConsEval a -> a
fromEval = fromEval'
toEval :: ConsType a => a -> ConsEval a
toEval = toEval'
-- | Convert a heterogeneous list to a normal list
listHList :: ConsType a =>
(forall a . c (Full a) -> b) -> HList c a -> [b]
listHList = listHList'
-- | Convert a heterogeneous list to a normal list
listHListM :: (Monad m, ConsType a) =>
(forall a . c (Full a) -> m b) -> HList c a -> m [b]
listHListM = listHListM'
-- | Change the container of each element in a heterogeneous list
mapHList :: ConsType a =>
(forall a . c1 (Full a) -> c2 (Full a)) -> HList c1 a -> HList c2 a
mapHList = mapHList'
-- | Change the container of each element in a heterogeneous list, monadic
-- version
mapHListM :: (Monad m, ConsType a) =>
(forall a . c1 (Full a) -> m (c2 (Full a))) -> HList c1 a -> m (HList c2 a)
mapHListM = mapHListM'
-- | Apply the syntax tree to the listed arguments
appHList :: ConsType a =>
AST dom a -> HList (AST dom) a -> ASTF dom (EvalResult a)
appHList = appHList'
-- | Apply the evaluation function to the listed arguments
appEvalHList :: ConsType a =>
ConsEval a -> HList Identity a -> EvalResult a
appEvalHList = appEvalHList'
-- | Semantic constructor application
($:) :: (a :-> b) -> a -> b
Partial f $: a = f a
-- | Generic abstract syntax tree, parameterized by a symbol domain
--
-- In general, @(`AST` dom (a `:->` b))@ represents a partially applied (or
-- unapplied) constructor, missing at least one argument, while
-- @(`AST` dom (`Full` a))@ represents a fully applied constructor, i.e. a
-- complete syntax tree.
-- It is not possible to construct a total value of type @(`AST` dom a)@ that
-- does not fulfill the constraint @(`ConsType` a)@.
--
-- Note that the hidden class 'ConsType'' mentioned in the type of 'Symbol' is
-- interchangeable with 'ConsType'.
data AST dom a
where
Symbol :: ConsType' a => dom a -> AST dom a
(:$:) :: Typeable a => AST dom (a :-> b) -> ASTF dom a -> AST dom b
-- | Fully applied abstract syntax tree
type ASTF dom a = AST dom (Full a)
-- | Co-product of two symbol domains
data dom1 :+: dom2 :: * -> *
where
InjectL :: dom1 a -> (dom1 :+: dom2) a
InjectR :: dom2 a -> (dom1 :+: dom2) a
infixl 1 :$:
infixr :+:
-- | Class that performs the type-level recursion needed by 'appSym'
class ApplySym a f dom | a dom -> f, f -> a dom
where
appSym' :: AST dom a -> f
instance ApplySym (Full a) (ASTF dom a) dom
where
appSym' = id
instance (Typeable a, ApplySym b f' dom) =>
ApplySym (a :-> b) (ASTF dom a -> f') dom
where
appSym' sym a = appSym' (sym :$: a)
-- | Generic symbol application
--
-- 'appSym' has any type of the form:
--
-- > appSym :: (expr :<: AST dom, Typeable a, Typeable b, ..., Typeable x)
-- > => expr (a :-> b :-> ... :-> Full x)
-- > -> (ASTF dom a -> ASTF dom b -> ... -> ASTF dom x)
appSym :: (ApplySym a f dom, ConsType a, sym :<: AST dom) => sym a -> f
appSym sym = appSym' (inject sym)
-- | Generic symbol application with explicit context
appSymCtx :: (ApplySym a f dom, ConsType a, sym ctx :<: dom) =>
Proxy ctx -> sym ctx a -> f
appSymCtx _ = appSym
--------------------------------------------------------------------------------
-- * Subsumption
--------------------------------------------------------------------------------
class sub :<: sup
where
-- | Injection from @sub@ to @sup@
inject :: ConsType a => sub a -> sup a
-- | Partial projection from @sup@ to @sub@
project :: sup a -> Maybe (sub a)
instance (sub :<: sup) => ((:<:) sub (AST sup))
-- GHC 6.12 requires prefix syntax here
where
inject = Symbol . inject
project (Symbol a) = project a
project _ = Nothing
instance ((:<:) expr expr)
where
inject = id
project = Just
instance ((:<:) expr1 (expr1 :+: expr2))
where
inject = InjectL
project (InjectL a) = Just a
project _ = Nothing
instance (expr1 :<: expr3) => ((:<:) expr1 (expr2 :+: expr3))
where
inject = InjectR . inject
project (InjectR a) = project a
project _ = Nothing
-- | 'inject' with explicit context
injCtx :: (sub ctx :<: sup, ConsType a) => Proxy ctx -> sub ctx a -> sup a
injCtx _ = inject
-- | 'project' with explicit context
prjCtx :: (sub ctx :<: sup) => Proxy ctx -> sup a -> Maybe (sub ctx a)
prjCtx _ = project
--------------------------------------------------------------------------------
-- * Syntactic sugar
--------------------------------------------------------------------------------
-- | It is assumed that for all types @A@ fulfilling @(`Syntactic` A dom)@:
--
-- > eval a == eval (desugar $ (id :: A -> A) $ sugar a)
--
-- (using 'Language.Syntactic.Interpretation.Evaluation.eval')
class Typeable (Internal a) => Syntactic a dom | a -> dom
-- Note: using a functional dependency rather than an associated type,
-- because this makes it possible to make a class alias constraining dom.
-- GHC doesn't yet handle equality super classes.
where
type Internal a
desugar :: a -> ASTF dom (Internal a)
sugar :: ASTF dom (Internal a) -> a
instance Typeable a => Syntactic (ASTF dom a) dom
where
type Internal (ASTF dom a) = a
desugar = id
sugar = id
-- | Syntactic type casting
resugar :: (Syntactic a dom, Syntactic b dom, Internal a ~ Internal b) => a -> b
resugar = sugar . desugar
-- | N-ary syntactic functions
--
-- 'desugarN' has any type of the form:
--
-- > desugarN ::
-- > ( Syntactic a dom
-- > , Syntactic b dom
-- > , ...
-- > , Syntactic x dom
-- > ) => (a -> b -> ... -> x)
-- > -> ( AST dom (Full (Internal a))
-- > -> AST dom (Full (Internal b))
-- > -> ...
-- > -> AST dom (Full (Internal x))
-- > )
--
-- ...and vice versa for 'sugarN'.
class SyntacticN a internal | a -> internal
where
desugarN :: a -> internal
sugarN :: internal -> a
instance (Syntactic a dom, ia ~ AST dom (Full (Internal a))) => SyntacticN a ia
where
desugarN = desugar
sugarN = sugar
instance
( Syntactic a dom
, ia ~ Internal a
, SyntacticN b ib
) =>
SyntacticN (a -> b) (AST dom (Full ia) -> ib)
where
desugarN f = desugarN . f . sugar
sugarN f = sugarN . f . desugar
-- | \"Sugared\" symbol application
--
-- 'sugarSym' has any type of the form:
--
-- > sugarSym ::
-- > ( expr :<: AST dom
-- > , Syntactic a dom
-- > , Syntactic b dom
-- > , ...
-- > , Syntactic x dom
-- > ) => expr (Internal a :-> Internal b :-> ... :-> Full (Internal x))
-- > -> (a -> b -> ... -> x)
sugarSym
:: (ConsType a, expr :<: AST dom, ApplySym a b dom, SyntacticN c b)
=> expr a -> c
sugarSym = sugarN . appSym
-- | \"Sugared\" symbol application with explicit context
sugarSymCtx
:: (ConsType a, sym ctx :<: dom, ApplySym a b dom, SyntacticN c b)
=> Proxy ctx -> sym ctx a -> c
sugarSymCtx _ = sugarSym
--------------------------------------------------------------------------------
-- * AST processing
--------------------------------------------------------------------------------
newtype Const a b = Const {unConst :: a}
-- Only used in the definition of 'queryNodeSimple'
newtype WrapAST c dom a = WrapAST { unWrapAST :: c (AST dom a) }
-- Only used in the definition of 'transformNode'
-- | Query an 'AST' using a function that gets direct access to the top-most
-- constructor and its sub-trees
--
-- Note that, by instantiating the type @c@ with @`AST` dom'@, we get the
-- following type, which shows that 'queryNode' can be directly used to
-- transform syntax trees (see also 'transformNode'):
--
-- > (forall a . ConsType a => dom a -> HList (AST dom) a -> ASTF dom' (EvalResult a))
-- > -> ASTF dom a
-- > -> ASTF dom' a
queryNode :: forall dom a c
. (forall a . ConsType a =>
dom a -> HList (AST dom) a -> c (Full (EvalResult a)))
-> ASTF dom a
-> c (Full a)
queryNode f a = query a Nil
where
query :: AST dom b -> HList (AST dom) b -> c (Full (EvalResult b))
query (Symbol a) args = f a args
query (c :$: a) args = query c (a :*: args)
-- | A simpler version of 'queryNode'
--
-- This function can be used to create 'AST' traversal functions indexed by the
-- symbol types, for example:
--
-- > class Count subDomain
-- > where
-- > count' :: Count domain => subDomain a -> HList (AST domain) a -> Int
-- >
-- > instance (Count sub1, Count sub2) => Count (sub1 :+: sub2)
-- > where
-- > count' (InjectL a) args = count' a args
-- > count' (InjectR a) args = count' a args
-- >
-- > count :: Count dom => ASTF dom a -> Int
-- > count = queryNodeSimple count'
--
-- Here, @count@ represents some static analysis on an 'AST'. Each constructor
-- in the tree will be queried by @count'@ indexed by the corresponding symbol
-- type. That way, @count'@ can be seen as an open-ended function on an open
-- data type. The @(Count domain)@ constraint on @count'@ is to allow recursion
-- over sub-trees.
--
-- Let's say we have a symbol
--
-- > data Add a
-- > where
-- > Add :: Add (Int :-> Int :-> Full Int)
--
-- Then the @Count@ instance for @Add@ might look as follows:
--
-- > instance Count Add
-- > where
-- > count' Add (a :*: b :*: Nil) = 1 + count a + count b
queryNodeSimple :: forall dom a b
. (forall a . ConsType a => dom a -> HList (AST dom) a -> b)
-> ASTF dom a
-> b
queryNodeSimple f a = unConst $ queryNode (\c -> Const . f c) a
-- | A version of 'queryNode' where the result is a transformed syntax tree,
-- wrapped in a type constructor @c@
transformNode :: forall dom dom' c a
. ( forall a . ConsType a
=> dom a -> HList (AST dom) a -> c (ASTF dom' (EvalResult a))
)
-> ASTF dom a
-> c (ASTF dom' a)
transformNode f a = unWrapAST $ queryNode (\a args -> WrapAST (f a args)) a
--------------------------------------------------------------------------------
-- * Restricted syntax trees
--------------------------------------------------------------------------------
-- | An abstract representation of a constraint on @a@. An instance might look
-- as follows:
--
-- > instance MyClass a => Sat MyContext a
-- > where
-- > data Witness MyContext a = MyClass a => MyWitness
-- > witness = MyWitness
--
-- This allows us to use @(`Sat` MyContext a)@ instead of @(MyClass a)@. The
-- point with this is that @MyContext@ can be provided as a parameter, so this
-- effectively allows us to parameterize on class constraints. Note that the
-- existential context in the data definition is important. This means that,
-- given a constraint @(`Sat` MyContext a)@, we can always construct the context
-- @(MyClass a)@ by calling the 'witness' method (the class instance only
-- declares the reverse relationship).
--
-- This way of parameterizing over type classes was inspired by
-- /Restricted Data Types in Haskell/ (John Hughes, /Haskell Workshop/, 1999).
class Sat ctx a
where
data Witness ctx a
witness :: Witness ctx a
witnessByProxy :: Sat ctx a => Proxy ctx -> Proxy a -> Witness ctx a
witnessByProxy _ _ = witness
-- | Witness of a @(`Sat` ctx a)@ constraint. This is different from
-- @(`Witness` ctx a)@, which witnesses the class encoded by @ctx@. 'Witness''
-- has a single constructor for all contexts, while 'Witness' has different
-- constructors for different contexts.
data SatWit ctx a
where
SatWit :: Sat ctx a => SatWit ctx a
fromSatWit :: SatWit ctx a -> Witness ctx a
fromSatWit SatWit = witness
-- | Expressions that act as witnesses of their result type
class WitnessSat expr
where
type SatContext expr
witnessSat :: expr a -> SatWit (SatContext expr) (EvalResult a)
-- | Expressions that act as witnesses of their result type
class MaybeWitnessSat ctx expr
where
maybeWitnessSat :: Proxy ctx -> expr a -> Maybe (SatWit ctx (EvalResult a))
instance MaybeWitnessSat ctx dom => MaybeWitnessSat ctx (AST dom)
where
maybeWitnessSat ctx (Symbol a) = maybeWitnessSat ctx a
maybeWitnessSat ctx (f :$: _) = maybeWitnessSat ctx f
instance (MaybeWitnessSat ctx sub1, MaybeWitnessSat ctx sub2) =>
MaybeWitnessSat ctx (sub1 :+: sub2)
where
maybeWitnessSat ctx (InjectL a) = maybeWitnessSat ctx a
maybeWitnessSat ctx (InjectR a) = maybeWitnessSat ctx a
-- | Convenient default implementation of 'maybeWitnessSat'
maybeWitnessSatDefault :: WitnessSat expr
=> Proxy (SatContext expr)
-> expr a
-> Maybe (SatWit (SatContext expr) (EvalResult a))
maybeWitnessSatDefault _ = Just . witnessSat
-- | Type application for constraining the @ctx@ type of a parameterized symbol
withContext :: sym ctx a -> Proxy ctx -> sym ctx a
withContext = const
-- | Representation of a fully polymorphic constraint -- i.e. @(`Sat` `Poly` a)@
-- is satisfied by all types @a@.
data Poly
instance Sat Poly a
where
data Witness Poly a = PolyWit
witness = PolyWit
poly :: Proxy Poly
poly = Proxy
-- | Representation of \"simple\" types: types satisfying
-- @(`Eq` a, `Show` a, `Typeable` a)@
data SimpleCtx
instance (Eq a, Show a, Typeable a) => Sat SimpleCtx a
where
data Witness SimpleCtx a = (Eq a, Show a, Typeable a) => SimpleWit
witness = SimpleWit
simpleCtx :: Proxy SimpleCtx
simpleCtx = Proxy