Компилятор SystemT и работа с бесконечными типами в Haskell

Я слежу за этой записью в блоге: http://semantic-domain.blogspot.com/2012/12/total-functional-programming-in-partial.html

На нем показан небольшой компилятор OCaml для System T (простой полностью функциональный язык).

Весь конвейер использует синтаксис OCaml (через метапрограммирование Camlp4), преобразует их в OCaml AST, который транслируется в лямбда-исчисление SystemT (см.: module Term), а затем, наконец, в комбинаторное исчисление SystemT (см.: module Goedel). Последний шаг также обернут метапрограммированием OCaml типа Ast.expr.

Я пытаюсь перевести его на Haskell без использования Template Haskell.

Для формы SystemT Combinator я написал это как

{-# LANGUAGE GADTs #-}

data TNat = Zero | Succ TNat

data THom a b where
  Id :: THom a a
  Unit :: THom a ()
  ZeroH :: THom () TNat
  SuccH :: THom TNat TNat
  Compose :: THom a b -> THom b c -> THom a c
  Pair :: THom a b -> THom a c -> THom a (b, c)
  Fst :: THom (a, b) a
  Snd :: THom (a, b) b
  Curry :: THom (a, b) c -> THom a (b -> c)
  Eval :: THom ((a -> b), a) b -- (A = B) * A -> B
  Iter :: THom a b -> THom (a, b) b -> THom (a, TNat) b

Обратите внимание, что Compose — это прямая композиция, которая отличается от (.).

Во время преобразования лямбда-исчисления SystemT в комбинаторное исчисление SystemT функция Elaborate.synth пытается преобразовать переменные лямбда-исчисления SystemT в ряд составных проекционных выражений (связанных с индексами Де Брюжина). Это связано с тем, что комбинаторное исчисление не имеет имен переменных/переменных. Это делается с помощью Elaborate.lookup, который использует функцию Quote.find.

Проблема в том, что с моей кодировкой комбинаторного исчисления как GADT THom a b. Перевод функции Quote.find:

  let rec find x  = function
    | [] -> raise Not_found
    | (x', t) :: ctx' when x = x' -> <:expr< Goedel.snd >> 
    | (x', t) :: ctx' -> <:expr< Goedel.compose Goedel.fst $find x ctx'$ >>

В Хаскеле:

find :: TVar -> Context -> _
find tvar [] = error "Not Found"
find tvar ((tvar', ttype):ctx)
  | tvar == tvar' = Snd
  | otherwise     = Compose Fst (find tvar ctx)

Приводит к ошибке бесконечного типа.

• Occurs check: cannot construct the infinite type: a ~ (a, c)
  Expected type: THom (a, c) c
    Actual type: THom ((a, c), c) c

Проблема связана с тем, что использование Compose, Fst и Snd из THom a b GADT может привести к бесконечным вариациям подписи типа.

В статье нет этой проблемы, потому что она использует Ast.expr OCaml для переноса основных выражений.

Я не уверен, как разрешить в Haskell. Должен ли я использовать экзистенциально квантифицированный тип, например

data TExpr = forall a b. TExpr (THom a b)

Или какой-то уровень типа Fix для адаптации проблемы бесконечного типа. Однако я не уверен, как это меняет функции find или lookup.


person CMCDragonkai    schedule 17.11.2018    source источник
comment
На самом деле тип не совпадает. Учтите: tvar == tvar' = Snd указывает, что find имеет тип find :: TVar -> Context -> THom (a, b) b, следовательно, (find tvar ctx) имеет тот же тип. поэтому тип аргументов Compose Fst (find tvar ctx) как compose (Thom (a, b) a) (Thom (a, b) b), Обратите внимание, что в определении compose нужно: a == (a, b), но это невозможно.   -  person assembly.jc    schedule 17.11.2018
comment
Да именно в этом проблема. И бесконечная ошибка типа.   -  person CMCDragonkai    schedule 17.11.2018
comment
Нам нужно иметь возможность написать тип для find :: TVar -> Context -> ???. Простое, но неэстетичное решение — отказаться от GADT для THom (и внедрить пристрастность повсюду). Другое решение состоит в том, чтобы попытаться стать полностью зависимым, введя больше индексов: что-то вроде find :: TVar v -> Context c -> THom (F v c) с некоторым семейством типов F. Я уверен, что в Agda/Coq мы могли бы написать это, если бы у нас было достаточно времени. В Haskell нам, вероятно, нужно больше параметров доказательства, одноэлементных типов, если предположить, что это вообще выполнимо (я не совсем уверен).   -  person chi    schedule 17.11.2018
comment
Но какова цель функции find?   -  person assembly.jc    schedule 17.11.2018
comment
@assembly.jc Грубо говоря, доступ к переменной компилируется в последовательность парных проекций. Контекст представляет собой вложенную пару, похожую на список, например (((),x),y), и для доступа к x вы хотите использовать что-то вроде fst;snd АКА отбросить последнюю переменную, взять следующую. Как правило, в более длинных контекстах вы генерируете fst;fst;....;fst;snd, чтобы отбросить многие переменные и взять в конце ту, которую вы хотите. Весь смысл этого заключается в том, чтобы мы выставляли больше информации в типе, в отличие, например. используя обычный список. Однако этот подход требует большого количества зависимых функций.   -  person chi    schedule 17.11.2018
comment
Чтобы преобразовать лямбда-переменные системы T в проекционные выражения в комбинаторном исчислении системы T.   -  person CMCDragonkai    schedule 17.11.2018
comment
Согласно примечанию в связанном сообщении, вы должны запретить бесконечно рекурсивные THom. Вы должны сделать все рекурсивные вхождения строгими. Это не ухудшает и не улучшает проблему, с которой вы столкнулись, просто делает перевод ближе. (По крайней мере, я думаю, что добавление строгости решает проблему. Автор говорит, что лень вызывает пристрастие, но не предлагает решения).   -  person HTNW    schedule 18.11.2018
comment
@chi Пристрастность неизбежна для того, что делает исходная статья, поскольку она анализирует и проверяет типы потенциально некорректных выражений. Вариант с зависимой типизацией, вероятно, будет возможен (с возрастающими усилиями) в Agda/Idris, в Haskell (с большим трудом), в Coq с Equations (плагин для функциональности, подобной GADT) или в простом Coq (где он самоубийство, так как чистый Coq не удобно поддерживает зависимое сопоставление с образцом).   -  person Blaisorblade    schedule 18.11.2018
comment
@Blaisorblade Вы говорите, что зависимое сопоставление с образцом более удобно в Agda, чем в (простом) Coq? Если да, то почему? (Связано с аксиомой K?) Я не настоящий эксперт, но по своему опыту я лучше понял сопоставление Coq, так что теперь мне любопытно.   -  person chi    schedule 18.11.2018
comment
@Blaisorblade Я не понимаю, что ты говоришь. Я просто комментировал тот факт, что автор связанного поста отметил, что такие вещи, как let bad = Compose bad SuccH in bad, должны быть запрещены. Я думаю, что либеральное применение аннотаций ! должно устранять такие вещи, например, что если fun :: THom a b, то либо fun = _|_, либо fun является реальным, полным, тотальным, 100%-ным функционалом, без каких-либо пристрастий, в любом месте представления функции от a до b.   -  person HTNW    schedule 18.11.2018
comment
@HTNW Извините, вы уже написали, что это не ухудшает и не улучшает проблему, с которой вы столкнулись, я пропустил это. Удалил свой предыдущий комментарий.   -  person Blaisorblade    schedule 18.11.2018
comment
@чи Да. Некоторые определяют зависимое сопоставление с образцом как аналог того, что GHC делает для GADT (хотя я полагаю, что GADT появились позже), так что простой Coq не имеет этого, и поэтому были написаны уравнения. Я не говорю, что это тривиально в использовании, но гораздо удобнее, как только вы его изучите. Одной из исторических причин была аксиома K, но после появления теории гомотопических типов люди провели исследования в поддержку зависимого сопоставления с образцом, не обязательно полагаясь на аксиому K (с ограничениями) или доказывая ограничения этой аксиомы. для конкретных типов данных, где это действительно так.   -  person Blaisorblade    schedule 18.11.2018
comment
@chi Одна неприятность с сопоставлением шаблонов, зависящим от Agda, заключается в том, что его поведение полностью предсказуемо в соответствии с относительно простыми правилами, но почти никто не объяснил их (когда я узнал Agda), поэтому некоторые сбои кажутся загадочными.   -  person Blaisorblade    schedule 18.11.2018


Ответы (1)


Этот ответ должен быть немного высокоуровневым, потому что есть три совершенно разных семейства возможных дизайнов для решения этой проблемы. То, что вы делаете, кажется ближе к третьему подходу, но подходы упорядочены по возрастанию сложности.

Подход в исходном посте

Для перевода исходного поста требуется Template Haskell и пристрастность; find вернет Q.Exp, представляющий некоторое Hom a b, избегая этой проблемы, как и в исходном сообщении. Как и в исходном посте, ошибка типа в исходном коде будет обнаружена при проверке типов вывода всех функций Template Haskell. Таким образом, ошибки типов по-прежнему обнаруживаются до среды выполнения, но вам все равно нужно будет написать тесты, чтобы найти случаи, когда ваши макросы выводят выражения с неправильным типом. Можно дать более сильные гарантии ценой значительного увеличения сложности.

Зависимая типизация/GADT на вводе и выводе

Если вы хотите отклониться от поста, один из вариантов — использовать «зависимую типизацию» повсюду и сделать ввод зависимо типизированным. Я использую этот термин в широком смысле, чтобы включить в себя как на самом деле языки с зависимой типизацией, так и реальный зависимый Haskell (когда он приземлится), а также способы имитации зависимой типизации в Haskell сегодня (через GADT, синглтоны и т. д.). Однако вы теряете возможность написать свою собственную программу проверки типов и выбрать, какую систему типов использовать; как правило, вы в конечном итоге встраиваете просто типизированное лямбда-исчисление и можете имитировать полиморфизм с помощью полиморфных функций Haskell, которые могут генерировать термины заданного типа. Это проще в языках с зависимой типизацией, но вообще возможно в Haskell.

Но, честно говоря, на этом пути проще использовать абстрактный синтаксис более высокого порядка и функции Haskell с чем-то вроде:

data Exp a where
  Abs :: (Exp a -> Exp b) -> Exp (a -> b)
  App :: Exp (a -> b) -> Exp a -> Exp b
  Var :: String -> Exp a —- only use for free variables
exampleId :: Exp (a -> a)
exampleId = Abs (\x -> x)

Если вы можете использовать этот подход (подробности здесь опущены), вы получаете высокую надежность от GADT с ограниченной сложностью. Однако этот подход слишком негибок для многих сценариев, например, потому что контексты типизации видны только компилятору Haskell, а не в ваших типах или терминах.

От нетипизированных терминов к типизированным

Третий вариант — использовать зависимую типизацию и, чтобы ваша программа по-прежнему преобразовывала слабо типизированный ввод в строго типизированный вывод. В этом случае ваша программа проверки типов полностью преобразует термины некоторого типа Expr в термины GADT TExp gamma t, Hom a b или подобные. Поскольку вы не знаете статически, что такое gamma и t (или a и b), вам действительно понадобится какой-то экзистенциал.

Но простой экзистенциал слишком слаб: чтобы построить более крупное хорошо типизированное выражение, вам нужно проверить, что создаваемые типы это позволяют. Например, чтобы построить TExpr, содержащее выражение Compose, из двух меньших TExpr, вам нужно проверить (во время выполнения), что их типы совпадают. А с простым экзистенциалом нельзя. Поэтому вам нужно иметь представление типов также на уровне значений.

Более того, экзистенциалы раздражают (как обычно), поскольку вы никогда не можете раскрыть переменные скрытого типа в возвращаемом типе или спроецировать их (в отличие от зависимых записей/сигма-типов). Я, честно говоря, не совсем уверен, что это может в конечном итоге заставить работать. Вот возможный частичный набросок на Haskell, до одного случая find.

data Type t where
  VNat :: Type Nat
  VString :: Type String
  VArrow :: Type a -> Type b -> Type (a -> b)
  VPair :: Type a -> Type b -> Type (a, b) 
  VUnit :: Type ()
data SomeType = forall t. SomeType (Type t)
data SomeHom = forall a b. SomeHom (Type a) (Type b) (THom a b)

type Context = [(TVar, SomeType)] 
getType :: Context -> SomeType
getType [] = SomeType VUnit 
getType ((_, SomeType ttyp) :: gamma) =  
   case getType gamma of
       SomeType ctxT -> SomeType (VPair ttyp
find :: TVar -> Context -> SomeHom 
find tvar ((tvar’, ttyp) :: gamma)
   | tvar == tvar’ =
       case (ttyp, getType gamma) of
         (SomeType t, SomeType ctxT) ->
          SomeHom (VPair t ctxT) t Fst
person Blaisorblade    schedule 17.11.2018
comment
Причина, по которой я не использую Template Haskell, заключается в том, что мне не нужен встроенный DSL, я собираюсь использовать его для внешнего языка. В этой ситуации вы рекомендуете свое третье решение? Конечно, Haskell можно использовать для написания этого простого компилятора. - person CMCDragonkai; 18.11.2018
comment
Итак, вы рекомендуете третье решение, но оно не простое? - person CMCDragonkai; 19.11.2018
comment
Ах; тогда я обычно рекомендую вообще избегать GADT, как это делает большинство компиляторов для внешних языков (включая GHC), особенно если вы действительно хотите использовать полученный код (а не играть с ним). Третий вариант может сработать, но он не самый быстрый: некоторые операции с хорошо типизированными терминами (такие как подстановка) могут оказаться намного сложнее реализовать; вам может повезти. - person Blaisorblade; 19.11.2018
comment
Пожалуйста, не обращайте внимания на комментарий, который я сейчас удалил — я разместил его по ошибке при редактировании. - person Blaisorblade; 19.11.2018
comment
Хорошо, если не GADT, что вы предлагаете в качестве альтернативного кодирования комбинаторного исчисления System T? - person CMCDragonkai; 19.11.2018
comment
@CMCDragonkai К сожалению, меня не уведомили — я бы использовал простые алгебраические типы данных. По сути, просто удалите параметры типа из Expr/THom и т. д. Еще несколько функций станут частичными, и вам потребуются дополнительные тесты. Вы по-прежнему можете хранить типы (язык объекта) внутри AST, чтобы вы могли запускать средство проверки типов, которое проверяет (во время выполнения), что ваш AST может быть типизирован в System T. Это наиболее распространенный выбор в компиляторах/интерпретаторах. - person Blaisorblade; 22.11.2018