ГАДЦ для DSL: качели и карусели? - PullRequest
0 голосов
/ 28 февраля 2019

Типичные примеры преимуществ GADT представляют синтаксис для DSL;скажем здесь, на вики или бумаге PLDI 2005 .

Я могу видеть, что если у вас есть AST, который по типу корректен по конструкции, пишите evalФункция проста.

Как встроить обработку GADT в REPL?Или, более конкретно, в цикл чтения-разбора-проверки типа-проверки-печати?Я вижу, что вы просто подталкиваете сложность от шага eval к более ранним шагам.

Использует ли GHCi внутренний GADT для представления выражений, которые он оценивает?(Выражения намного короче, чем типичные DSL.)

  • С одной стороны, вы не можете derive Show для GADT, поэтому для шага печати вы либо катитесь вручнуюShow экземпляров или что-то вроде этого:

    {-# LANGUAGE  GADTs, StandaloneDeriving  #-}
    
    data Term a where
      Lit :: Int -> Term Int
      Inc :: Term Int -> Term Int
      IsZ :: Term Int -> Term Bool
      If :: Term Bool -> Term a -> Term a -> Term a
      Pair :: (Show a, Show b) => Term a -> Term b -> Term (a,b)
      Fst :: (Show b) => Term (a,b) -> Term a
      Snd :: (Show a) => Term (a,b) -> Term b
    
    deriving instance (Show a) => Show (Term a)
    

    (Мне кажется, что эти Show ограничения, запутанные в конструкторах, уже не в состоянии разделить проблемы.)

Я больше думаю о пользовательском опыте, когда кто-то вводит выражения DSL, а не об удобстве программиста функцией eval.Либо:

  • Пользователь вводит выражения непосредственно с помощью конструкторов GADT.Легко сделать синтаксически правильную, но неправильно напечатанную ошибку (скажем, неправильно поставленные парены).Тогда GHCi дает довольно недружелюбные сообщения об отказе.Или
  • REPL принимает ввод как текст и анализирует его.Но для GADT, подобного этому, получение экземпляра Read - это действительно тяжелая работа.Так что, возможно,
  • Приложение имеет две структуры данных: одна допускает ошибки типа;другой - ГАДТ;и шаг проверки создает GADT AST, если он может сделать это безошибочно с типом.

В этой последней статье я, кажется, вернулся с «умными конструкторами», которые GADT должны улучшить(?) Более того, я где-то удвоил работу.

У меня нет «лучшего способа» подойти к ней.Мне интересно, как на практике подходить к приложениям DSL.(Для контекста: я имею в виду среду запросов к базе данных, где вывод типов должен смотреть на типы полей в базе данных, чтобы проверить, какие операции над ними.)

Addit: после проработки ответа от @ Alec

Я вижу, что код для красивой печати в glambda включает несколько слоев классов и экземпляров.Здесь что-то не так, в отличие от заявленных преимуществ GADT для AST.Идея (хорошо типизированного) AST заключается в том, что вы можете одинаково легко: оценить его;или просто распечатай;или оптимизировать его;или генерировать код из него;и т. д.

glambda, кажется, нацелены на повышение квалификации (что достаточно справедливо с учетом цели упражнения).Мне интересно ...

  • Зачем нужно выражать весь синтаксис для (E) DSL в одном типе данных?(Пример из викибук начинает, когда соломенный человек делает это data Expr = ...; и быстро сталкивается с проблемами типа. Ну, конечно, это так; это никогда не сработает; почти все будет работать лучше этого; я чувствую себя обманутым.)

  • Если мы все равно в конечном итоге пишем классы и экземпляры, почему бы не сделать каждый синтаксис производящим отдельный тип данных: data Lit = Lit Int ... data If b a1 a2 = If b a1 a2 ... Тогда a class IsTerm a c | a -> c where ... (то есть a FunDep или, возможно, семейство типов, экземпляры которого сообщают нам тип результата термина.)

  • Теперь в EDSL используются одни и те же конструкторы (пользователю не важно, что они имеют разные типы данных);и они применяют «небрежную» проверку типов.Хорошая печать / отчеты об ошибках также не требуют строгой проверки типов.Эвал делает это и настаивает на том, чтобы все экземпляры IsTerm были выстроены в ряд.

Я не предлагал такой подход раньше, потому что он, казалось, содержал слишком много грубого кода.Но на самом деле это не хуже, чем glambda - то есть, когда вы рассматриваете всю функциональность, а не только шаг eval.

Мне кажется большим преимуществом выражать синтаксис только один раз.Более того, это кажется более расширяемым: добавьте новый тип данных для каждого синтаксического производства, а не взламывайте существующий тип данных.Да, и, поскольку они являются типами данных H98 (без экзистенциалов), deriving отлично работает.

1 Ответ

0 голосов
/ 28 февраля 2019

Обратите внимание, что GHCi не использует GADT для представления выражений.Даже внутренний тип выражения GHC Expr не является GADT.

DSL

Для того, чтобы иметь более крупный, более конкретный пример типа Termрассмотрим glambda.Его тип Exp даже отслеживает переменные на уровне типа.

  • Существует второй тип данных UExp, который, как вы сами заметили, является тем, что получаетфактически разбирается из REPL.Затем этот тип проверяется на тип Exp и передается в продолжение с:

    check :: (MonadError Doc m, MonadReader Globals m)
          => UExp -> (forall t. STy t -> Exp '[] t -> m r)
          -> m r
    
  • Довольно печатная надпись UExp и Exp написана от руки, но припо крайней мере использует тот же код (он делает это через класс PrettyExp).

  • Сам код оценки прекрасен, но ясомневаюсь, что мне нужно продать тебя на этом.:)

EDSL

Насколько я понимаю, GADT великолепны для EDSL (встроенных DSL), поскольку это всего лишь части кода в большой программе на Haskell.Да, ошибки типов могут быть сложными (и будут исходить от GHC напрямую), но это цена, которую вы платите за возможность поддерживать инварианты на уровне типов в своем коде.Рассмотрим, например, представление основных блоков в CFG в hoopl:

data Block n e x where
  BlockCO  :: n C O -> Block n O O          -> Block n C O
  BlockCC  :: n C O -> Block n O O -> n O C -> Block n C C
  BlockOC  ::          Block n O O -> n O C -> Block n O C

  BNil    :: Block n O O
  BMiddle :: n O O                      -> Block n O O
  BCat    :: Block n O O -> Block n O O -> Block n O O
  BSnoc   :: Block n O O -> n O O       -> Block n O O
  BCons   :: n O O       -> Block n O O -> Block n O O

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

Ну и что ...?

Я пытаюсь сказать следующее: если ваш GADT создается из String (или пользовательского REPL), выбудет нелегко выполнить перевод.Это неизбежно, потому что то, что вы делаете, - это, по сути, повторная реализация простой средства проверки типов. Лучше всего вам противостоять этому (как это делает glambda) и отличать синтаксический анализ от проверки типов.

Однако, если вы можете позволить себе оставаться в рамках кода на Haskell, вы можете просто выполнить синтаксический анализ и проверку типов в GHC.ИМХО, EDSL намного круче и практичнее, чем не встроенные DSL.

...