I would like to extend @Daniel Wagner's excellent answer with a slightly different approach: instead of typechecking returning a valid type (if any), return a typed expression that is then guaranteed, we can evaluate it (since a simple-typed lambda calculus is very normal). The main idea is that check ctx te returns Just (ctx |- e :: t) iff e can be entered in t in some ctx context and then some typed expression ctx |- e :: t , we can evaluate him in some Env ironing the right type.
Implementation
I will use singletones to emulate the type Pi check :: (ctx :: [Type]) -> (a :: Type) -> Term -> Maybe (TTerm ctx a) , which means that we will need to enable each GHC extension and kitchen sink:
{-# LANGUAGE GADTs #-} {-# LANGUAGE DataKinds, KindSignatures, TypeFamilies, TypeOperators #-} {-# LANGUAGE TemplateHaskell #-} -- sigh... import Data.Singletons.Prelude import Data.Singletons.TH import Data.Type.Equality
The first bit is an untyped representation, right from Daniel Wagner's answer:
data Type = Base | Arrow Type Type deriving (Show, Eq) data Term = Const | Var Int | Lam Type Term | App Term Term deriving Show
but we will also give semantics for these types, interpreting Base as () and Arrow t1 t2 as t1 -> t2 :
type family Interp (t :: Type) where Interp Base = () Interp (Arrow t1 t2) = Interp t1 -> Interp t2
To keep de Bruin's theme, contexts are a list of types, and variables are context indexes. Given the context type environment, we can find the index of the variable to get the value. Please note that lookupVar is a complete function.
data VarIdx (ts :: [Type]) (a :: Type) where Here :: VarIdx (a ': ts) a There :: VarIdx ts a -> VarIdx (b ': ts) a data Env (ts :: [Type]) where Nil :: Env '[] Cons :: Interp a -> Env ts -> Env (a ': ts) lookupVar :: VarIdx ts a -> Env ts -> Interp a lookupVar Here (Cons x _) = x lookupVar (There v) (Cons _ xs) = lookupVar v xs
OK, we have all the infrastructure to actually write code. First of all, we define a typed representation of Term together with an evaluator (total!):
data TTerm (ctx :: [Type]) (a :: Type) where TConst :: TTerm ctx Base TVar :: VarIdx ctx a -> TTerm ctx a TLam :: TTerm (a ': ctx) b -> TTerm ctx (Arrow ab) TApp :: TTerm ctx (Arrow ab) -> TTerm ctx a -> TTerm ctx b eval :: Env ctx -> TTerm ctx a -> Interp a eval env TConst = () eval env (TVar v) = lookupVar v env eval env (TLam lam) = \x -> eval (Cons x env) lam eval env (TApp fe) = eval env f $ eval env e
So far so good. eval is nice and total because its input can only display typed members of a simply typed lambda calculus. Therefore, part of the work of the appraiser @ Daniel must be done when converting an untyped representation to a printed one.
The main idea of infer is that if type inference succeeds, it returns Just $ TheTerm te , where t is Sing lithuanian witness of type e .
$(genSingletons [''Type]) $(singDecideInstance ''Type) -- I wish I had sigma types... data SomeTerm (ctx :: [Type]) where TheTerm :: Sing a -> TTerm ctx a -> SomeTerm ctx data SomeVar (ctx :: [Type]) where TheVar :: Sing a -> VarIdx ctx a -> SomeVar ctx -- ... and pi ones as well infer :: Sing ctx -> Term -> Maybe (SomeTerm ctx) infer _ Const = return $ TheTerm SBase TConst infer ts (Var n) = do TheVar tv <- inferVar ts n return $ TheTerm t $ TVar v infer ts (App fe) = do TheTerm t0 e' <- infer ts e TheTerm (SArrow t0' t) f' <- infer ts f Refl <- testEquality t0' t0 return $ TheTerm t $ TApp f' e' infer ts (Lam ty e) = case toSing ty of SomeSing t0 -> do TheTerm te' <- infer (SCons t0 ts) e return $ TheTerm (SArrow t0 t) $ TLam e' inferVar :: Sing ctx -> Int -> Maybe (SomeVar ctx) inferVar (SCons t _) 0 = return $ TheVar t Here inferVar (SCons _ ts) n = do TheVar tv <- inferVar ts (n-1) return $ TheVar t $ There v inferVar _ _ = Nothing
We hope that the last step is obvious: since we can evaluate only a typed term in a given type (since this gives us the type of its Haskell embedding), we return the infer ence type to the check ING type:
check :: Sing ctx -> Sing a -> Term -> Maybe (TTerm ctx a) check ctx te = do TheTerm t' e' <- infer ctx e Refl <- testEquality tt' return e'
Session Example
Let's try our functions in GHCi:
λ» :set -XStandaloneDeriving -XGADTs λ» deriving instance Show (VarIdx ctx a) λ» deriving instance Show (TTerm ctx a) λ» let id = Lam Base (Var 0) -- \x -> x λ» check SNil (SBase `SArrow` SBase) id Just (TLam (TVar Here)) λ» let const = Lam Base $ Lam Base $ Var 1 -- \xy -> x λ» check SNil (SBase `SArrow` SBase) const Nothing -- Oops, wrong type λ» check SNil (SBase `SArrow` (SBase `SArrow` SBase)) const Just (TLam (TLam (TVar Here))) λ» :t eval Nil <$> check SNil (SBase `SArrow` (SBase `SArrow` SBase)) const eval Nil <$> check SNil (SBase `SArrow` (SBase `SArrow` SBase)) const :: Maybe (() -> () -> ()) -- Note that the `Maybe` there comes from `check`, not `eval`! λ» let Just const' = check SNil (SBase `SArrow` (SBase `SArrow` SBase)) const λ» :t eval Nil const' eval Nil const' :: () -> () -> () λ» eval Nil const' () () ()