Following on from this q about GADTs, I'm trying to build an EDSL (for the example in the paper) but without GADTs. I have got something working that avoids doubling-up the datatypes for the AST; but instead it seems to be doubling-up the code. So I tried trimming down the code, which is where I've run into trouble ...
Instead of a GADT like so
data Term a where
Lit :: Int -> Term Int
Inc :: Term Int -> Term Int
IsZ :: Term Int -> Term Bool
-- etc
declare each constructor as a separate datatype
data Lit = Lit Int deriving (Show, Read)
data Inc a = Inc a deriving (Show, Read)
data IsZ a = IsZ a deriving (Show, Read)
-- etc
Then the EDSL user can enter and show terms
aTerm = IsZ (Inc (Lit 5))
illtypedTerm = Inc (IsZ (Lit 5)) -- accepted OK and can show
-- but can't eval [good!]
Then to catch that they're all Terms and need to be well-typed
data Term a = ToTerm { fromTerm :: a} deriving (Show, Eq)
class IsTerm a c | a -> c
instance IsTerm Lit (Term Int)
-- etc
The FunDep captures the return type from the original GADT. Then eval can use that Term type
class Eval a
where eval :: (IsTerm a c) => a -> c
-- hmm this makes 'c' a rigid tyvar
instance Eval Lit where
eval (Lit i) = -- undefined -- accepted OK, infers :: Term Int
ToTerm i -- rejected
The equation eval (Lit i) = undefined (commented out) compiles OK, and GHC infers eval (Lit 5) :: Term Int. But if I put = ToTerm i:
* Couldn't match expected type `c' with actual type `Term Int'
`c' is a rigid type variable bound by
the type signature for:
eval :: forall c. IsTerm Lit c => Lit -> c
* Relevant bindings include
eval :: Lit -> c
If GHC can infer (via the FunDep) that c must be Term Int for = undefined, why can't it for = ToTerm i? Is the specialised type sig it's inferred eval :: forall c. IsTerm Lit c => Lit -> c Impredicative? But c is the return type, so it's not RankN(?)
What avoids this error? I do have working
class (IsTerm a c) => Eval a c | a -> c where ...this just duplicates all the instance heads fromIsTerm, so the superclass constraint is only acting as belt and braces. (That's the doubling-up I was trying to avoid.)type family ToTerm a ...; class Eval a where eval :: a -> ToTerm a. But again the instances forEvaldouble-up all the instances forToTerm, and furthermore need large contexts with many~constraints betweenToTermcalls.
I could just throw away class IsTerm, and put all the Term-inference on class Eval. But I was trying to closely mirror the GADT style such that I might have many application 'clients' sharing the same definition for Term.
Addit: [March 14]
The 2011 paper System F with Type Equality Coercions, Section 2.3, has this example (in discussing Functional Dependencies)
class F a b | a -> b
instance F Int Bool
class D a where { op :: F a b => a -> b }
instance D Int where { op _ = True }
Using FC, this problem [of typing the definition of
opin the instanceD Int] is easily solved: the coercion in the dictionary forFenables the result ofopto be cast to typebas required.
This example seems to be the same as the q, with class
F, with FunDep, beingIsTermand classDbeingEval.This example doesn't compile: gives the same rejection as
IsTerm/Eval.
ToTerm iis not allowing just any type forc. OTOH it is allowing any type that meets theIsTerm a c =>constraint on methodeval. So(?) GHC is ignoring the constraint? Or isn't clever enough to see the constraint has a FunDep that improvescto exactly the type ofToTerm i. - AntC