{-# OPTIONS -fglasgow-exts #-}
{-# OPTIONS -fallow-undecidable-instances #-}
-- Regardless of what the above declaration says, all our instances
-- here *are* decidable. By `decidable instances' GHC means
-- instances whose termination can be _easily_ decided locally,
-- on the per-instance basis. Sometimes we need more extensive analysis.
-- Big-Lambda HOAS
-- Even higher-order abstract syntax: are GADTs compelling?
{-
We show the typeclass implementation of the example used to make the
case for GADTs. We demonstrate the higher-order abstract-syntax-based
embedding of a language in Haskell and implement static and dynamic
semantics of the language. The interpreter of the language is tagless
and statically assured: Only well-typed terms may be evaluated, and
the evaluation of those does not get stuck. We use no tags and *no*
run-time pattern-matching, therefore, the `eval' function has no
possibility of raising a run-time error. Our language is
_non_-strongly normalizing and non-structurally inductive due to the
presence of Fix; yet the typechecking is decidable and our typeclass
programs always terminate.
The running example has been used as an argument for generalized
recursive data types (one of the variants of GADTs). This
message is meant to clarify the expressiveness of GADTs with respect
to typeclasses. Our implementation is also aimed at the sometimes
exuberant enthusiasm for GADTs.
In positive contribution, we demonstrate the _automatic_ promotion of
a value-level higher-order abstract syntax (HOAS) term to the
type-level. The small-lambda HOAS becomes the big-Lambda HOAS and can
be operated by the typechecker. We demonstrate the typechecking of our
term language by abstract interpretation. The latter is performed by
the Haskell typechecker itself, following straightforward declarative
rules expressed via instance declarations. The code works in GHC and
Hugs.
Please see the accompanying article for more.
-}
module HOASL where
-- The following helper class represents (generally, untyped)
-- functions in the object language. The end user will NOT write
-- any instances of the class, so this class does not have to be exported.
-- This class is instrumental in automatical lifting
-- of Haskell functions into the object language, or the `lifting' of
-- the value-level polymorphic functions onto the type level.
-- That makes the big-lambda explicit and permits applications to it.
class Apply f x y | f x -> y where
apply :: f -> x -> y
-- The identity `function'. It is introduced for illustration only.
-- The end users will use the generic instance below for all their needs.
data ID
instance Apply ID x x where
apply _ x = x
-- `Lifting' of ordinary, value-level polymorphic functions onto
-- the `type level', to permit their typechecking by abstract
-- interpretation.
-- The TypeCast below signifies the application of the Big Lambda.
instance TypeCast x x1 => Apply (x1 -> y) x y where
apply f x = f (typeCast x)
-- The syntax of our terms
-- The terms containing binders (like Lam and Fix) should have the type
-- annotation on the bound variable (just as is the case for the code
-- in the GRDT paper). That type, however, can be inferred in many cases, as
-- the examples below show.
newtype Lit a = Lit a -- aka, Lift in GRDT
newtype Inc a = Inc a
newtype IsZ a = IsZ a
newtype If a b c = If (a,b,c)
newtype Fst a = Fst a
newtype Snd a = Snd a
newtype Pair a b = Pair (a,b)
-- Higher-order terms
newtype Lam f t = Lam f -- t is the type of the bound arg
newtype App a b = App (a,b)
newtype Fix f t = Fix f -- ditto
-- Hypothetical reasoning: used for typechecking functions
-- (aka typechecking by abstract interpretation).
newtype AbsValue t = AbsValue t
-- Static semantics: well-formedness rules (aka, typing rules)
-- Of a t
-- asserts the term a has the type t
class Of a t | a -> t
instance Of (Lit t) t
instance Of a Int => Of (Inc a) Int
instance Of a Int => Of (IsZ a) Bool
instance (Of t Bool, Of b1 a, Of b2 a) => Of (If t b1 b2) a
instance (Of t1 a, Of t2 b) => Of (Pair t1 t2) (a,b)
instance Of t (a,b) => Of (Fst t) a
instance Of t (a,b) => Of (Snd t) b
instance (Of a (t1->t2), Of b t1) => Of (App a b) t2
-- The Hyp elimination rule: we get what we assumed
instance Of (AbsValue t) t
-- The Hyp introduction rule: typechecking functions.
-- The function mapping the term `a' to the term 'b'
-- has the type t1->t2, if the function maps the term AbsValue t1
-- to the term that has the type t2.
-- In other words, if the body of the function has the type t2
-- _assuming_ the argument has the type t1.
instance (Apply f (AbsValue t1) y, Of y t2) => Of (Lam f t1) (t1->t2)
-- Ditto for the type-checking of Fix. Using of the AbsValue makes the
-- typechecking of the HOAS Fix terminating! Although we lack
-- structural induction, we do maintain decidability.
instance (Apply f (AbsValue t) y, Of y t) => Of (Fix f t) t
teval :: Of e t => e -> t -- static evaluator
teval = undefined -- all computation is static
-- Dynamic semantics: big-step evaluation
class Eval a t | a->t where
eval:: a -> t
instance Of (Lit a) a => Eval (Lit a) a where
eval (Lit i) = i
instance Eval t Int => Eval (Inc t) Int where
eval (Inc t) = eval t + 1
instance Eval t Int => Eval (IsZ t) Bool where
eval (IsZ t) = eval t == 0
instance (Eval t Bool, Eval b1 a, Eval b2 a) => Eval (If t b1 b2) a where
eval (If (t,b1,b2)) = if eval t then eval b1 else eval b2
instance (Eval t1 a, Eval t2 b) => Eval (Pair t1 t2) (a,b) where
eval (Pair (t1,t2)) = (eval t1, eval t2)
instance Eval t (a,b) => Eval (Fst t) a where
eval (Fst t) = fst (eval t)
instance Eval t (a,b) => Eval (Snd t) b where
eval (Snd t) = snd (eval t)
instance (Eval a (t1->t2), Eval b t1) => Eval (App a b) t2 where
eval (App (a,b)) = (eval a) (eval b)
instance (Apply f (Lit t1) b, Eval b t2)
=> Eval (Lam f t1) (t1->t2) where
eval (Lam f) = \x -> eval (apply f (Lit x))
-- Here, we convert the Fix to the value-level fix
instance (Apply f (Lit t) y, Eval y t)
=> Eval (Fix f t) t where
eval (Fix f) = fix (\x -> eval $ apply f (Lit x))
fix f = f (fix f)
-- Tests
tl1 = teval $ Lam (undefined::ID)
t2 = eval $ Fix (undefined::ID)
-- the test from the first-order language
t3 = If ( (IsZ (Fst (Pair (Lit (0::Int), Lit (1::Int)))))
, (Inc (Fst (Pair (Lit (0::Int), Lit (1::Int)))))
, (Inc (Snd (Pair (Lit (0::Int), Lit (1::Int))))) )
et3 = eval t3 -- 1
-- Make t3 higher-order
t3f p = If ( (IsZ (Fst p)), (Inc (Fst p)), (Inc (Snd p)) )
et4f1 = teval $ Lam t3f
et4f2 = eval $ Lam t3f
et4f3 = eval $ App (Lam t3f, (Pair (Lit (0::Int), Lit (1::Int))))
t5f = Lam $ \p -> If( IsZ p, (Pair (p,Inc p)), (Pair (p,Fix id)) )
et5f1 = teval $ t5f
et5f2 = eval $ t5f
et5f3 = eval $ App (t5f, Lit (0::Int)) -- yields (0,1)
et5f4t= teval $ App (t5f, Lit (1::Int)) -- typechecking terminates
et5f4 = eval $ App (t5f, Lit (1::Int)) -- evaluation diverges!
-- The final test, that using Fix in a meaningful way.
-- We implement the addition: x + y -> z
-- Because we have the increment but don't have decrement, the
-- first argument must be given with the opposite sign.
t7 self = Lam f
where
f p = -- p is a (Pair x y) where x is negative
If( IsZ (Fst p), Snd p,
App (self, Pair ((Inc (Fst p)),Inc (Snd p))))
et71 = teval (Lam t7)
et72 = teval (Fix t7)
et73 = eval $ App (Fix t7,Pair(Lit(-5),Lit(4)))
-- to make the code self-contained
class TypeCast a b | a -> b, b->a where typeCast :: a -> b
class TypeCast' t a b | t a -> b, t b -> a where typeCast' :: t->a->b
class TypeCast'' t a b | t a -> b, t b -> a where typeCast'' :: t->a->b
instance TypeCast' () a b => TypeCast a b where typeCast x = typeCast' () x
instance TypeCast'' t a b => TypeCast' t a b where typeCast' = typeCast''
instance TypeCast'' () a a where typeCast'' _ x = x