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 Term
s 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 forEval
double-up all the instances forToTerm
, and furthermore need large contexts with many~
constraints betweenToTerm
calls.
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
op
in the instanceD Int
] is easily solved: the coercion in the dictionary forF
enables the result ofop
to be cast to typeb
as required.
This example seems to be the same as the q, with class
F
, with FunDep, beingIsTerm
and classD
beingEval
.This example doesn't compile: gives the same rejection as
IsTerm/Eval
.
ToTerm i
is 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 improvesc
to exactly the type ofToTerm i
. – Ermey