Haskell is particularly capable of modeling your domain, perhaps because it can be described using a fairly simple mathematical model. Essentially, your first point means that the subtype relation is well-order . It makes your life very simple - this model will probably easily go into any language whose type system is at least as strong as that of Haskell.
Start by defining the type (which will be filmed as) to present your options:
data Variant = Primary | Unary | Mult | Add | Comp | Expr
The following is a non-recursive data type for representing nodes in a language of terms:
data ExprF (k :: Variant -> *) (x :: Variant) where ID_F :: ExprF k 'Primary Paren_F :: k 'Expr -> ExprF k 'Primary Negate_F :: k 'Primary -> ExprF k 'Unary Mult_F :: k 'Mult -> k 'Unary -> ExprF k 'Mult Add_F :: k 'Add -> k 'Mult -> ExprF k 'Add Comp_F :: k 'Add -> k 'Add -> ExprF k 'Comp
Recursive occurrences of terms are represented by an additional parameter. Essentially, this is just a typical representation of the polynomial functional (i.e., Fix ), but with an index.
Then your expression type:
data Expr' (x :: Variant) where Expr' :: (x <= y) => Expr x -> Expr' y data Expr (x :: Variant) where MkExpr :: ExprF Expr' x -> Expr x
The class <= has not yet been entered, but it represents your subtype relationship.
As mentioned earlier, your subtype relationship is a good order, and by this virtue, each element in the ordering can be assigned a unique natural number, such that a typical order in natural relations corresponds to your subtype. Or, in other words, there is an injection f : Variant -> Nat such that x is a subtype of y iff fx <= fy (or a strict subtype of iff fx < fy - this representation gives you a lot of commonality).
The necessary injection is given only by your grammar. Please note that each statement is only a "subtype" (i.e., has the right part, which the constructor should not introduce) over production over it.
data Nat = Z | S Nat infixr 0 $ type ($) fa = fa type family VariantIx (x :: Variant) :: Nat where VariantIx 'Primary = 'Z VariantIx 'Unary = 'Z VariantIx 'Mult = $ 'Z VariantIx 'Add = $ $ 'Z VariantIx 'Comp = $ $ $ 'Z VariantIx 'Expr = $ $ $ $ 'Z
You need an implicit subtype relation (which is <= ), but it is often much easier to work with explicit proof of the relationship, so it is typical that the implicit version simply generates an explicit proof. For this purpose, you write two announcements:
data family (:<=:) (x :: k) (y :: k) class (<=) (x :: k) (y :: k) where isLTEQ :: x :<=: y
Instances for naturals should be fairly obvious:
data instance (:<=:) (x :: Nat) y where LT_Z :: 'Z :<=: n LT_S :: n :<=: m -> n :<=: m instance 'Z <= n where isLTEQ = LT_Z instance (n <= m) => n <= m where isLTEQ = LT_S isLTEQ
and instances for Variant determine the order caused by VariantIx :
newtype instance (:<=:) (x :: Variant) y = IsSubtype (VariantIx x :<=: VariantIx y) instance (VariantIx x <= VariantIx y) => x <= y where isLTEQ = IsSubtype isLTEQ
You probably need smart constructors. If you use the recent GHC, you will have access to template synonyms, but this is optional:
id_ = MkExpr ID_F pattern Id = MkExpr ID_F pattern Paren e = MkExpr (Paren_F (Expr' e)) pattern Neg e = MkExpr (Negate_F (Expr' e)) infixl 6 :+ pattern (:+) ab = MkExpr (Add_F (Expr' a) (Expr' b)) infixl 7 :* pattern (:*) ab = MkExpr (Mult_F (Expr' a) (Expr' b)) pattern Cmp ab = MkExpr (Comp_F (Expr' a) (Expr' b))
and some simple examples:
>Id :+ Id :+ Neg Id :* Id Add_F (Add_F ID_F ID_F) (Mult_F (Negate_F ID_F) ID_F) >Id :+ Id :* Neg (Id :* Id) <interactive>:6:13: No instance for (( $ 'Z) <= 'Z) arising from a use of `Neg'
Note that you can also write your type of expression in a slightly different way:
data ExprFlip (x :: Variant) where MkExprFlip :: (x <= y) => ExprF ExprFlip x -> ExprFlip y
This differs from the original in that the most external type of expression has a subtype relation applied to it - for example,
pattern Id' = MkExprFlip ID_F
is of type ExprFlip t , and Id :: Expr 'Primary . I do not see another way in which they differ, and I believe that this would simply be a matter of preference or the most common use cases. The initial representation has the advantage that the type of inference is always monomorphic, which in some cases can make type inference better, but does not affect the construction of expressions.
To answer your four questions:
- This model relies on the semantics of the relationship of the subtype of design.
VariantIx and type Variant are closed. Any additional instances for :<=: or <= for Variant or Nat will overlap with existing ones (as common as possible), therefore, in principle, they can be defined, an attempt to use them will create type errors.- Essentially, you have a reflexive and transitive relation, and these properties are fixed in the instance
<= for Nat once and for all. Changing the relationship of a subtype comes down only to changing Variant and VariantIx . - The proofs of the subtype relation are constructed according to the type of inference - the class
<= . Since all indexes in the ExprF type are monomorphic, the type checker can always calculate the subtype ratio for the indexes.
Full code:
{-# LANGUAGE StandaloneDeriving, UndecidableInstances, PatternSynonyms , TypeOperators, KindSignatures, PolyKinds, DataKinds, GADTs, TypeFamilies , MultiParamTypeClasses, FlexibleContexts, FlexibleInstances #-} data Variant = Primary | Unary | Mult | Add | Comp | Expr data ExprF (k :: Variant -> *) (x :: Variant) where ID_F :: ExprF k 'Primary Paren_F :: k 'Expr -> ExprF k 'Primary Negate_F :: k 'Primary -> ExprF k 'Unary Mult_F :: k 'Mult -> k 'Unary -> ExprF k 'Mult Add_F :: k 'Add -> k 'Mult -> ExprF k 'Add Comp_F :: k 'Add -> k 'Add -> ExprF k 'Comp data Expr' (x :: Variant) where Expr' :: (x <= y) => Expr x -> Expr' y data Expr (x :: Variant) where MkExpr :: ExprF Expr' x -> Expr x data ExprFlip (x :: Variant) where MkExprFlip :: (x <= y) => ExprF ExprFlip x -> ExprFlip y pattern Id' = MkExprFlip ID_F data Nat = Z | S Nat infixr 0 $ type ($) fa = fa type family VariantIx (x :: Variant) :: Nat where VariantIx 'Primary = 'Z VariantIx 'Unary = 'Z VariantIx 'Mult = $ 'Z VariantIx 'Add = $ $ 'Z VariantIx 'Comp = $ $ $ 'Z VariantIx 'Expr = $ $ $ $ 'Z data family (:<=:) (x :: k) (y :: k) class (<=) (x :: k) (y :: k) where isLTEQ :: x :<=: y data instance (:<=:) (x :: Nat) y where LT_Z :: 'Z :<=: n LT_S :: n :<=: m -> n :<=: m instance 'Z <= n where isLTEQ = LT_Z instance (n <= m) => n <= m where isLTEQ = LT_S isLTEQ newtype instance (:<=:) (x :: Variant) y = IsSubtype (VariantIx x :<=: VariantIx y) instance (VariantIx x <= VariantIx y) => x <= y where isLTEQ = IsSubtype isLTEQ id_ = MkExpr ID_F pattern Id = MkExpr ID_F pattern Paren e = MkExpr (Paren_F (Expr' e)) pattern Neg e = MkExpr (Negate_F (Expr' e)) infixl 6 :+ pattern (:+) ab = MkExpr (Add_F (Expr' a) (Expr' b)) infixl 7 :* pattern (:*) ab = MkExpr (Mult_F (Expr' a) (Expr' b)) pattern Cmp ab = MkExpr (Comp_F (Expr' a) (Expr' b)) instance Show (Expr' x) where showsPrec k (Expr' x) = showsPrec kx instance Show (Expr x) where showsPrec k (MkExpr x) = showsPrec kx deriving instance (Show (k 'Mult), Show (k 'Add), Show (k 'Expr), Show (k 'Primary), Show (k 'Unary)) => Show (ExprF kx)