Разбор свободных монад

Скажем, у меня есть следующая свободная монада:

data ExampleF a
  = Foo Int a
  | Bar String (Int -> a)
  deriving Functor

type Example = Free ExampleF  -- this is the free monad want to discuss

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

foo :: Int -> Example ()
foo i = liftF $ Foo i ()

bar :: String -> Example Int
bar s = liftF $ Bar s id

Поэтому я могу писать программы на хаскеле, например:

fooThenBar :: Example Int
fooThenBar =
  do
    foo 10
    bar "nice"

Я знаю, как его распечатать, интерпретировать и т. д. Но как насчет его разбора?

Можно ли написать синтаксический анализатор, который мог бы анализировать произвольные программы, такие как:

foo 12
bar nice
foo 11
foo 42

Поэтому я могу хранить их, сериализовать, использовать в программах cli и т. д.

Проблема, с которой я постоянно сталкиваюсь, заключается в том, что тип программы зависит от того, какая программа анализируется. Если программа заканчивается на foo, она имеет тип Example (), если она заканчивается на bar, она имеет тип Example Int.

Мне не хочется писать синтаксические анализаторы для каждой возможной перестановки (здесь это просто, потому что есть только две возможности, но представьте, что мы добавляем Baz Int (String -> a), Doo (Int -> a), Moz Int a, Foz String a, .... Это утомительно и подвержено ошибкам).

Может я не ту задачу решаю?

Шаблон

Чтобы запустить приведенные выше примеры, вам нужно добавить это в начало файла:

{-# LANGUAGE DeriveFunctor #-}

import Control.Monad.Free
import Text.ParserCombinators.Parsec

Примечание. Я разместил суть, содержащую этот код.


person romeovs    schedule 06.01.2016    source источник
comment
лучше всего загрузить полностью рабочий пример, например. lpaste.net или для нескольких файлов gist.github.com   -  person Erik Kaplun    schedule 06.01.2016
comment
Предположим, вы можете написать парсер, который возвращает forall a. Example a. Что бы вы сделали с результатом?   -  person n. 1.8e9-where's-my-share m.    schedule 06.01.2016
comment
@н.м. Он должен использоваться в TCP-взаимодействии. Клиент отправляет команды, а сервер отвечает на них. Ваш вопрос заставляет меня чувствовать, что я не совсем его понимаю ・‿・ Может быть, мне следует разобрать Example Response или что-то вроде Response a => Example a?   -  person romeovs    schedule 06.01.2016
comment
Итак, вы хотите Example Response. Это намного проще. Что произойдет, если строка действительно будет разобрана на Example Int? Ошибка разбора. Ваш парсер должен уметь обрабатывать ошибки, не так ли?   -  person n. 1.8e9-where's-my-share m.    schedule 06.01.2016
comment
Хорошо, но тогда последний аргумент для каждого конструктора ExampleF должен быть (Response -> a) правильным. Это теряет большую часть выразительности монады Example. Тогда это больше похоже на язык без типов, когда Response просто впитывает все возможные типы... Хм, надо подумать над этим.   -  person romeovs    schedule 06.01.2016
comment
По сути, вы ожидаете, что ваш синтаксический анализатор также выполнит проверку типов для вас - ваше примерное выражение допустимо для одного типа Example (), но не для Example Int - поэтому, если у вас есть синтаксический анализатор типа forall a . Parser (Example a), тогда этот синтаксический анализатор должен различать на основе типа, но он явно делает нет (параметрический в a). Итак, вам нужна такая функция, как TypeWitness a -> Parser (Example a), где TypeWitness — это GADT, проиндексированный для вашего языка типов, — или вы просто пишете одну функцию синтаксического анализатора для каждого типа, что является более простым решением.   -  person user2407038    schedule 06.01.2016


Ответы (2)


Не каждое значение 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 поэтому называется зависимой парой или сигма типом — тип второго компонента пары зависит от значения первого. Это распространенная техника: когда вы стираете параметр типа с помощью квантификации существования, обычно выгодно сделать его восстанавливаемым, объединив время выполнения, представляющее этот параметр.

Обратите внимание, что это использование 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
comment
хороший ответ! Вы, вероятно, правы, что вместо этого мне просто нужен Free Applicative. Я все еще новичок в типе Zoo, поэтому мне трудно найти свой путь. Более конкретно. Мне просто нужно иметь возможность писать простые встроенные программы, и я хотел частично зафиксировать их семантику (какая команда может вернуть какое значение) в хорошем смысле, поэтому я наткнулся на Free монады, которые, кажется, все реклама. Возможно, больше людей должны писать хорошие статьи и сообщения в блогах на Free applicative! - person romeovs; 07.01.2016
comment
Честно говоря, я попытался использовать Free Monads только из-за этого поста. Трудно понять, дают ли Free Applicatives те же преимущества, что и в статье для Free Applicatives (в основном наличие нескольких независимых интерпретаторов, которые можно компоновать). - person romeovs; 07.01.2016
comment
Если все, что вам нужно, это Applicative, то это то, что вы должны использовать. Каждый Monad является Applicative, но не каждый Applicative является Monad; разница в том, что Monad позволяет будущим вычислениям зависеть от прошлых результатов (тогда как Applicative эффекты являются чисто статическими). Applicative сочинить легче, чем Monad, а не сложнее. - person Benjamin Hodgson♦; 07.01.2016
comment
Знаете ли вы, что операторы ввода больше не должны начинаться с двоеточия? Это делает такой код еще лучше. - person luqui; 09.01.2016

Итак, я беспокоюсь, что это та же самая преждевременная абстракция, которую вы видите в объектно-ориентированных языках, мешающая работе. Например, я не уверен на 100%, что вы используете структуру свободной монады — ваши помощники, например, просто используют id и () довольно скучным образом, на самом деле я не уверен, что ваша Int -> x когда-либо ничего, кроме Pure :: Int -> Free ExampleF Int или const (something :: Free ExampleF Int).

Свободная монада для функтора F в основном может быть описана как дерево, данные которого хранятся в листьях и коэффициент ветвления которого контролируется рекурсией в каждом конструкторе функтора F. Так, например, Free Identity не имеет ветвления, следовательно, только один лист, и, таким образом, имеет ту же структуру, что и монада:

data MonoidalFree m x = MF m x deriving (Functor)
instance Monoid m => Monad (MonoidalFree m) where
    return x = MF mempty x
    MF m x >>= my_x = case my_x x of MF n y -> MF (mappend m n) y

На самом деле Free Identity изоморфно MonoidalFree (Sum Integer), разница только в том, что вместо MF (Sum 3) "Hello" вы видите Free . Identity . Free . Identity . Free . Identity $ Pure "Hello" как средство отслеживания этого целого числа. С другой стороны, если у вас есть data E x = L x | R x deriving (Functor), то вы получите своего рода «путь» Ls и Rs, прежде чем вы наткнетесь на этот один лист, Free E будет изоморфен MonoidalFree [Bool].

Причина, по которой я это делаю, заключается в том, что когда вы комбинируете Free с функтором Integer -> x, вы получаете бесконечно разветвляющееся дерево, и когда я просматриваю ваш код, чтобы выяснить, как вы < strong>фактически используя это дерево, все, что я вижу, это то, что вы используете с ним функцию id. Насколько я могу судить, это ограничивает рекурсию либо формой Free (Bar "string" Pure), либо формой Free (Bar "string" (const subExpression)),, и в этом случае система, по-видимому, полностью сводится к монаде MonoidalFree [Either Int String].

(Здесь я должен остановиться, чтобы спросить: насколько вам известно, это правильно? Было ли это задумано?)

В любом случае. Помимо моих проблем с вашей преждевременной абстракцией, конкретная проблема, которую вы цитируете с вашей монадой (вы не можете определить разницу между () и Int, имеет множество действительно сложных решений, но одно очень простое. Действительно простое решение состоит в том, чтобы получить значение типа Example (Either () Int), и если у вас есть (), вы можете fmap Left перейти к нему, а если у вас есть Int, вы можете fmap Right к нему.

Без лучшего понимания того, как вы используете эту штуку через TCP/IP, мы не можем порекомендовать вам лучшую структуру, чем общие бесплатные монады, которые вы, кажется, находите — в частности, мы нужно знать, как вы планируете использовать бесконечное ветвление Int -> x опций на практике.

person CR Drost    schedule 06.01.2016
comment
Вау, это длинный ответ. Спасибо! Я пройду через это сегодня вечером, когда у меня будет время. - person romeovs; 06.01.2016