Не каждое значение Example
может быть представлено на странице без повторной реализации некоторой части Haskell. Например, return putStrLn
имеет тип Example (String -> IO ())
, но я не думаю, что имеет смысл пытаться разобрать такое значение Example
из файла.
Итак, давайте ограничимся анализом приведенных вами примеров, которые состоят только из вызовов foo
и bar
, последовательно соединенных с >>
(то есть без связывания переменных и без произвольных вычислений)*. Форма Бэкуса-Наура для нашей грамматики выглядит примерно так:
<program> ::= "" | <expr> "\n" <program>
<expr> ::= "foo " <integer> | "bar " <string>
Достаточно просто разобрать наши два типа выражений...
type Parser = Parsec String ()
int :: Parser Int
int = fmap read (many1 digit)
parseFoo :: Parser (Example ())
parseFoo = string "foo " *> fmap foo int
parseBar :: Parser (Example Int)
parseBar = string "bar " *> fmap bar (many1 alphaNum)
... но как мы можем придать тип составу этих двух парсеров?
parseExpr :: Parser (Example ???)
parseExpr = parseFoo <|> parseBar
parseFoo
и parseBar
имеют разные типы, поэтому мы не можем составить их с помощью <|> :: Alternative f => f a -> f a -> f a
. Кроме того, нет никакого способа узнать заранее, какого типа будет программа, которую нам дали: как вы указываете, тип анализируемой программы зависит от значения входной строки. Типы, зависящие от значений, называются зависимыми типами; В Haskell нет надлежащей системы зависимых типов, но она достаточно близка к тому, чтобы мы попытались заставить этот пример работать.
Давайте начнем с того, что заставим выражения по обе стороны от <|>
иметь один и тот же тип. Это включает удаление параметра типа Example
с помощью количественная оценка существования.†
data Ex a = forall i. Wrap (a i)
parseExpr :: Parser (Ex Example)
parseExpr = fmap Wrap parseFoo <|> fmap Wrap parseBar
Это проверяет тип, но синтаксический анализатор теперь возвращает Example
, содержащее значение неизвестного типа. Значение неизвестного типа, конечно, бесполезно, но мы знаем кое-что о параметре Example
: это должно быть либо ()
, либо Int
, потому что это возвращаемые типы parseFoo
и parseBar
. Программирование — это перенос знаний из вашего мозга на страницу, поэтому мы собираемся завершить значение Example
небольшим количеством GADT свидетельство, которое при раскрытии покажет вам, было ли a
Int
или ()
.
data Ty a where
IntTy :: Ty Int
UnitTy :: Ty ()
data (a :*: b) i = a i :&: b i
type Sig a b = Ex (a :*: b)
pattern Sig x y = Wrap (x :&: y)
parseExpr :: Parser (Sig Ty Example)
parseExpr = fmap (\x -> Sig UnitTy x) parseFoo <|>
fmap (\x -> Sig IntTy x) parseBar
Ty
(что-то вроде) одноэлементного представления параметра типа Example
во время выполнения. При сопоставлении с образцом на IntTy
вы узнаете, что a ~ Int
; когда вы сопоставляете шаблон на UnitTy
, вы узнаете, что a ~ ()
. (Информацию можно заставить передаваться другим путем, от типов к значениям, используя классы.) :*:
, функтор-продукт объединяет два конструктора типов, обеспечивая равенство их параметров; таким образом, сопоставление с образцом на Ty
говорит вам о сопровождающем его Example
.
Sig
поэтому называется зависимой парой или сигма типом — тип второго компонента пары зависит от значения em> первого. Это распространенная техника: когда вы стираете параметр типа с помощью квантификации существования, обычно выгодно сделать его восстанавливаемым, объединив время выполнения, представляющее этот параметр.
Обратите внимание, что это использование Sig
эквивалентно Either (Example Int) (Example ())
- disjoint-union">тип сигмы - это сумма, в конце концов, но эта версия лучше масштабируется, когда вы суммируете большой (или, возможно, бесконечный) набор.
Теперь легко превратить наш анализатор выражений в анализатор программы. Нам просто нужно неоднократно применять анализатор выражений, а затем манипулировать зависимыми парами в списке.
parseProgram :: Parser (Sig Ty Example)
parseProgram = fmap (foldr1 combine) $ parseExpr `sepBy1` (char '\n')
where combine (Sig _ val) (Sig ty acc) = Sig ty (val >> acc)
Код, который я вам показал, не является образцовым. Он не разделяет задачи синтаксического анализа и проверки типов. В производственном коде я бы разбил этот дизайн на модули, сначала разобрав данные в нетипизированное синтаксическое дерево — отдельный тип данных, который не обеспечивает инвариант типизации, — а затем преобразовал его в типизированную версию, проверив ее тип. Техника зависимых пар по-прежнему будет необходима для придания типа выходным данным средства проверки типов, но она не будет запутана в синтаксическом анализаторе.
*Если привязка не является обязательной, задумывались ли вы об использовании бесплатное приложение для представления ваших данных?
†Ex
и :*:
— это детали механизма многократного использования, которые я взял из Документ по хазохизму
person
Benjamin Hodgson♦
schedule
06.01.2016
forall a. Example a
. Что бы вы сделали с результатом? - person n. 1.8e9-where's-my-share m.   schedule 06.01.2016Example Response
или что-то вродеResponse a => Example a
? - person romeovs   schedule 06.01.2016Example Response
. Это намного проще. Что произойдет, если строка действительно будет разобрана наExample Int
? Ошибка разбора. Ваш парсер должен уметь обрабатывать ошибки, не так ли? - person n. 1.8e9-where's-my-share m.   schedule 06.01.2016ExampleF
должен быть(Response -> a)
правильным. Это теряет большую часть выразительности монадыExample
. Тогда это больше похоже на язык без типов, когдаResponse
просто впитывает все возможные типы... Хм, надо подумать над этим. - person romeovs   schedule 06.01.2016Example ()
, но не дляExample Int
- поэтому, если у вас есть синтаксический анализатор типаforall a . Parser (Example a)
, тогда этот синтаксический анализатор должен различать на основе типа, но он явно делает нет (параметрический вa
). Итак, вам нужна такая функция, какTypeWitness a -> Parser (Example a)
, гдеTypeWitness
— это GADT, проиндексированный для вашего языка типов, — или вы просто пишете одну функцию синтаксического анализатора для каждого типа, что является более простым решением. - person user2407038   schedule 06.01.2016