Просто типизированное лямбда-исчисление с ошибкой на Haskell

Я новичок в Haskell, поэтому прошу прощения, если этот вопрос не имеет особого смысла.

Я хочу иметь возможность реализовать просто типизированные лямбда-выражения в Haskell таким образом, чтобы при попытке применить выражение к другому типу неправильного результатом была бы не ошибка типа, а скорее некоторая установить значение, например Ничего. Сначала я думал, что использование монады Maybe будет правильным подходом, но у меня ничего не получилось. Я задавался вопросом, что, если таковые имеются, было бы правильным способом сделать это.

Контекст проблемы, если это поможет, - это проект, над которым я работаю, который присваивает теги POS (часть речи) словам в предложениях. Для своего набора тегов я использую типы категориальной грамматики; это типизированные лямбда-выражения, такие как (e -> s) или (e -> (e -> s)), где e и s — это типы существительных и предложений соответственно. Так, например, kill имеет тип (e -> (e -> s)) — он принимает две словосочетания с существительными и возвращает предложение. Я хочу написать функцию, которая берет список объектов таких типов и выясняет, есть ли способ объединить их для достижения объекта типа s. Конечно, это как раз то, что в любом случае делает средство проверки типов Haskell, поэтому должно быть просто назначить каждому слову лямбда-выражение соответствующего типа, а Haskell сделает все остальное. Проблема в том, что если s не может быть достигнуто, проверка типов Haskell естественным образом останавливает выполнение программы.


person Reuben    schedule 08.01.2015    source источник
comment
Можете ли вы включить полный пример того, что вы пытаетесь сделать? Возможно, несколько слов, предложение, которое проверяет тип, и предложение, которое не проверяет, что вам нужно значение времени выполнения для сбоя, а не время компиляции? Если вы хотите узнать, есть ли способ объединить их для достижения типа, вы ищете какой-то поиск, который больше, чем то, что в настоящее время делает проверка типов Haskell (есть инструмент, который делает это с джином в его названии, но я не могу найти ссылку на него прямо сейчас).   -  person Cirdec    schedule 08.01.2015
comment
Ах, да, это инструмент djinn. Это связано с тем, что по небольшому набору типизированных объектов — юнит (), кортежи (,) и лямбда-конструктор для функций — он пытается вывести объект с заданным типом. Есть вопрос, связанный с о выводе кода Haskell из типов.   -  person Cirdec    schedule 08.01.2015
comment
Я не могу точно понять вашу цель. В частности, хотите ли вы, чтобы проверка типов выполнялась статически (перед запуском вашей программы) или динамически (во время выполнения). Ваше последнее утверждение о недостижимом s, кажется, указывает на то, что вам нужна динамическая проверка, но вы должны уточнить это.   -  person chi    schedule 08.01.2015


Ответы (2)


Довольно стандартные вещи. Просто напишите средство проверки типов и оценивайте термин только после проверки типов. evalMay делает это. Конечно, вы можете расширить набор констант и базовых типов; Я просто использовал один из каждого для простоты.

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
person Daniel Wagner    schedule 08.01.2015
comment
Может ли кто-нибудь прояснить, что происходит в случае subst n tm (Var m), особенно когда сравнение GT? Мне кажется, что либо m = n (так делаем подстановку), либо m <> n (так подстановка не действует), но результат ни в коем случае не должен быть Var (m-1). Может быть, это попытка выполнить каноническую операцию сдвига де Брейна? - person brianberns; 05.01.2021
comment
@brianberns Обратите внимание, что (кроме рекурсивных вызовов) subst вызывается только при оценке применения лямбды к термину. В этом случае мы собираемся стереть существование этой лямбды, следовательно, стереть существование переменной, которую она связывает. Итак, да, мы смещаемся, так что любые ссылки на переменные вне этой лямбды (скажем, потому что мы пытаемся оценить открытый термин) все равно будут указывать на нужное место. - person Daniel Wagner; 06.01.2021

Я хотел бы расширить отличный ответ @Daniel Wagner с немного другим подходом: вместо проверки типов, возвращающей допустимый тип (если он есть), возвращайте типизированное выражение, которое затем гарантирует, что мы можем его оценить (поскольку просто типизированная лямбда исчисление сильно нормализует). Основная идея состоит в том, что check ctx t e возвращает Just (ctx |- e :: t) тогда и только тогда, когда e может быть введено в t в некотором контексте ctx, а затем, имея некоторое типизированное выражение ctx |- e :: t, мы можем оценить его в некотором Envжелезе правильного типа.

Реализация

Я буду использовать синглтоны для эмуляции типа Pi check :: (ctx :: [Type]) -> (a :: Type) -> Term -> Maybe (TTerm ctx a), что означает, что нам нужно будет включить каждое расширение GHC и кухонную раковину:

{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds, KindSignatures, TypeFamilies, TypeOperators #-}
{-# LANGUAGE TemplateHaskell #-} -- sigh...

import Data.Singletons.Prelude
import Data.Singletons.TH
import Data.Type.Equality

Первый бит - это нетипизированное представление, прямо из ответа @Daniel Wagner:

data Type = Base
          | Arrow Type Type
          deriving (Show, Eq)

data Term = Const
          | Var Int
          | Lam Type Term
          | App Term Term
          deriving Show

но мы также дадим семантику для этих типов, интерпретируя Base как () и Arrow t1 t2 как t1 -> t2:

 type family Interp (t :: Type) where
    Interp Base = ()
    Interp (Arrow t1 t2) = Interp t1 -> Interp t2

Продолжая тему де Брейна, контексты — это список типов, а переменные — это индексы контекста. Учитывая среду типа контекста, мы можем найти индекс переменной, чтобы получить значение. Обратите внимание, что lookupVar — это суммарная функция.

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

Итак, у нас есть вся инфраструктура для написания кода. Прежде всего, давайте определим типизированное представление Term вместе с (всего!) оценщиком:

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

Все идет нормально. eval удобен и тотален, потому что его входные данные могут представлять только хорошо типизированные термины простого лямбда-исчисления. Таким образом, часть работы оценщика @Daniel должна быть выполнена по преобразованию нетипизированного представления в типизированное.

Основная идея infer заключается в том, что если вывод типа выполнен успешно, он возвращает Just $ TheTerm t e, где t — это Singleton-свидетель типа 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 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

Надеюсь, что последний шаг очевиден: поскольку мы можем оценить правильно типизированный терм только для заданного типа (поскольку именно это дает нам тип его внедрения в Haskell), мы превращаем тип inference в тип 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'

Пример сеанса

Давайте попробуем наши функции в 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' () ()
()
person Cactus    schedule 08.01.2015