Ответ 1
Нам нужен какой-то контекст, чтобы отслеживать аргументы лямбда. Однако нам не обязательно создавать их, поскольку bound
дает нам индексы де Брюина, и мы можем использовать эти индексы для индексации в контексте.
Фактически использование индексов немного связано с тем, что из-за механизма уровня типа, который отражает размер текущей области (или, другими словами, текущую глубину в выражении) через вложенность Var
- s. Это требует использования полиморфной рекурсии или GADT. Это также мешает нам хранить контекст в государственной монаде (потому что размер и, следовательно, тип контекста изменяется по мере того, как мы рекурсируем). Интересно, хотя если бы мы могли использовать индексированную государственную монаду; это был бы забавный эксперимент. Но я отвлекся.
Простейшим решением является представление контекста как функции:
type TC a = Either String a -- our checker monad
type Cxt a = a -> TC (Type a) -- the context
Ввод a
по существу является индексом de Bruijn, и мы просматриваем тип, применяя функцию к индексу. Мы можем определить пустой контекст следующим образом:
emptyCxt :: Cxt a
emptyCxt = const $ Left "variable not in scope"
И мы можем расширить контекст:
consCxt :: Type a -> Cxt a -> Cxt (Var () a)
consCxt ty cxt (B ()) = pure (F <$> ty)
consCxt ty cxt (F a) = (F <$>) <$> cxt a
Размер контекста кодируется в Var
вложенности. Увеличение размера видно здесь в обратном типе.
Теперь мы можем написать средство проверки типов. Главное, что мы используем fromScope
и toScope
, чтобы получить под связующими, и переносим по соответствующим расширенным Cxt
(чей тип строк просто отлично).
data Term a
= Var a
| Star -- or alternatively, "Type", or "*"
| Lam (Type a) (Scope () Term a)
| Pi (Type a) (Scope () Term a)
| App (Type a) (Term a)
deriving (Show, Eq, Functor)
-- boilerplate omitted (Monad, Applicative, Eq1, Show1 instances)
-- reduce to normal form
rnf :: Term a -> Term a
rnf = ...
-- Note: IIRC "Simply easy" and Augustsson post reduces to whnf
-- when type checking. I use here plain normal form, because it
-- simplifies the presentation a bit and it also works fine.
-- We rely on Bound alpha equality here, and also on the fact
-- that we keep types in normal form, so there no need for
-- additional reduction.
check :: Eq a => Cxt a -> Type a -> Term a -> TC ()
check cxt want t = do
have <- infer cxt t
when (want /= have) $ Left "type mismatch"
infer :: Eq a => Cxt a -> Term a -> TC (Type a)
infer cxt = \case
Var a -> cxt a
Star -> pure Star -- "Type : Type" system for simplicity
Lam ty t -> do
check cxt Star ty
let ty' = rnf ty
Pi ty' . toScope <$> infer (consCxt ty' cxt) (fromScope t)
Pi ty t -> do
check cxt Star ty
check (consCxt (rnf ty) cxt) Star (fromScope t)
pure Star
App f x ->
infer cxt f >>= \case
Pi ty t -> do
check cxt ty x
pure $ rnf (instantiate1 x t)
_ -> Left "can't apply non-function"
Здесь рабочий код, содержащий приведенные выше определения. Надеюсь, я не испортил это слишком плохо.