Simply typed lambda calculus with failure, in Haskell

后端 未结 2 737
生来不讨喜
生来不讨喜 2021-01-18 12:43

I\'m a newcomer to Haskell, so apologies if this question doesn\'t make too much sense.

I want to be able to implement simply typed lambda expressions in Haskell in

相关标签:
2条回答
  • 2021-01-18 13:09

    I'd like to extend @Daniel Wagner's excellent answer with a slightly different approach: instead of typechecking returning a valid type (if there is one), return a typed expression that is then guaranteed we can evaluate it (since the simply-typed lambda calculus is strongly normalizing). The basic idea is that check ctx t e returns Just (ctx |- e :: t) iff e can be typed at t in some context ctx, and then given some typed expression ctx |- e :: t, we can evaluate it in some Environment of the right type.

    The implementation

    I will be using singletons to emulate the Pi type of check :: (ctx :: [Type]) -> (a :: Type) -> Term -> Maybe (TTerm ctx a), which means we will need to turn on every GHC extension and the 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 the untyped representation, straight 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 by 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 with the de Bruijn theme, contexts are the list of types, and variables are indices of the context. Given an environment of a context type, we can look up a variable index to get a value. Note that lookupVar is a total 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 in place to actually write some code. First of all, let's define a typed representation of Term, together with a (total!) evaluator:

    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 a b)
        TApp :: TTerm ctx (Arrow a b) -> 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 f e) = eval env f $ eval env e
    

    So far so good. eval is nice & total because its input can only represent well-typed terms of the simply-typed lambda calculus. So part of the work from @Daniel's evaluator will have to be done in the transformation of the untyped representation to the typed one.

    The basic idea behind infer is that if type inference succeeds, it returns Just $ TheTerm t e, where t is a Singleton witness of e's type.

    $(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 t v <- inferVar ts n
        return $ TheTerm t $ TVar v
    infer ts (App f e) = 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 t e' <- 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 t v <- inferVar ts (n-1)
        return $ TheVar t $ There v
    inferVar _ _ = Nothing
    

    Hopefully the last step is obvious: since we can only evaluate a well-typed term at a given type (since that's what gives us the type of its Haskell embedding), we turn type inference into type checking:

    check :: Sing ctx -> Sing a -> Term -> Maybe (TTerm ctx a)
    check ctx t e = do
        TheTerm t' e' <- infer ctx e
        Refl <- testEquality t t'
        return e'
    

    Example session

    Let's try our functions out 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 -- \x y -> 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' () ()
    ()
    
    0 讨论(0)
  • 2021-01-18 13:21

    Pretty standard stuff. Just write a type-checker, and only evaluate the term when it typechecks. evalMay does this. You can of course enrich the set of constants and base types; I just used one of each for simplicity.

    import Control.Applicative ((<$), (<$>))
    import Control.Monad (guard)
    import Safe (atMay)
    
    data Type
        = Base
        | Arrow Type Type
        deriving (Eq, Ord, Read, Show)
    
    data Term
        = Const
        | Var Int -- deBruijn indexing; the nearest enclosing lambda binds Var 0
        | Lam Type Term
        | App Term Term
        deriving (Eq, Ord, Read, Show)
    
    check :: [Type] -> Term -> Maybe Type
    check env Const = return Base
    check env (Var v) = atMay env v
    check env (Lam ty tm) = Arrow ty <$> check (ty:env) tm
    check env (App tm tm') = do
        Arrow i o <- check env tm
        i' <- check env tm'
        guard (i == i')
        return o
    
    eval :: Term -> Term
    eval (App tm tm') = case eval tm of
        Lam _ body -> eval (subst 0 tm' body)
    eval v = v
    
    subst :: Int -> Term -> Term -> Term
    subst n tm Const = Const
    subst n tm (Var m) = case compare m n of
        LT -> Var m
        EQ -> tm
        GT -> Var (m-1)
    subst n tm (Lam ty body) = Lam ty (subst (n+1) tm body)
    subst n tm (App tm' tm'') = App (subst n tm tm') (subst n tm tm'')
    
    evalMay :: Term -> Maybe Term
    evalMay tm = eval tm <$ check [] tm
    
    0 讨论(0)
提交回复
热议问题