Пишу опердень на attoparsec. Т.к. парсер у нас одновременно и лексер, и собственно парсер, логику приходится перемежать обработкой пробельных символов:
pVarDecl :: Parser VariableDeclaration
pVarDecl = do
string "var"
skipWhitespace1 -- пропускаем 1 или больше пробельных символов
name <- pVarName
skipWhitespace -- пропускаем 0 или больше пробельных символов
value <- optional $ do
string "="
skipWhitespace
pExpression
skipWhitespace
string ";"
return $ VariableDeclaration name value
Это утомляет. Появляется закономерное желание «переопределить точку с запятой» и явно указывать только места, где пробельные символы обязательны:
pVarDecl :: Parser VariableDeclaration
pVarDecl = do
string "var"
requiredWhitespace
name <- pVarName
value <- optional $ do
string "="
pExpression
string ";"
return $ VariableDeclaration name value
(и потом ещё для полного счастья keyword k = string k >> requiredWhitespace
, да).
Удивительно, но сам attoparsec, похоже, ничего для этого не предлагает. Итак, какие у меня варианты?
Можно определить свою монаду. Это, конечно, круто, но придётся лифтить часть Data.Attoparsec. В принципе, это всё же лучше, чем ещё двести раз набрать «skipWhitespace», но все равно грязновато.
Есть ощущение, что можно обернуть парсер в трансформер, для которого определить инстанс Monad, и будет мне счастье. Но я трансформерами никогда не пользовался даже, не говоря уж о написании собственных; возможно, это бред, а не идея.
Есть у вас какие-то соображения на этот счёт?
юзать bison и не выебываться.
@ndtimofeev > Во-первых с такими вопросами тебе в juick.
Это потому, что там @qnikst и больше движухи, или это сейчас был посыл нахуй bnw-style?
Ну и что? Я же не хочу вмешиваться в их работу; мне нужно, чтобы
skipWhitespace
вставлялись между моими парсерами, только и всего.За указание в сторону language-lua и Text.Parsec.Token — спасибо.
Есть что получше?
тупой штоле
КОМПОЗИЦИЯ мазафака блядь, выкинь нахуй эти ебаные монадки, они в парсинге не нужны -- и осиль Applicative
@ndtimofeev хуй пососи, говно ему блядь
@ulidtko Насколько я понял, ты предлагаешь просто определить пару функций, с помощью которых абстрагировать пожирание пробельных символов? Вобщем-то вариант, спасибо!
@minoru да. в яблочко! ты был близок с
keyword k = string k >> requiredWhitespace
(таких функций для твоих вайтспейс-потребностей понадобится аж две.)и — я не могу сделать достаточное ударение: applicative parsing FTW
@minoru смотри, вот ты пишешь
— для этого не нужна аж монада. Эквивалентный код (всему do-блоку):
VariableDeclaration <$> foo <*> bar
. Всё. Монада не нужна, Applicative достаточно.@ulidtko Ок, убедил. Спасибо ещё раз :)
@minoru Хотя мне не сильно нравится, что мы больше никак не называем аргументы конструктора.
@minoru ты заебешься всё именовать в стиле name, value. Особенно на большой грамматике — попробуй написать в стиле одна строка парсера на одну строку грамматики (EBNF десу), поймёшь как охуенно
@minoru Поясняю суть Applicative на пальцах.
Нужно знать Functor. Это пререквизит. Его инстансы дают нам
fmap :: (a -> b) -> f a -> f b
. Это какmap
для списка, только для любого функтора вместо ссаного всех доебавшего уже списка. Позволяет промапить любую функцию «внутри» некого контекста. Если у меня естьfoobar :: Parser Int
, то(*2) <$> foobar
будет парсером, парсящим число — но возвращающим удвоенное. Значок(<$>) = fmap
.Applicative — подкласс Functor. Значит, в Applicative у нас всегда есть
fmap
, а также:pure
и(*)
(apply). Сигнатураpure :: Applicative f => a -> f a
— значит кладет «чистое» значениеa
(без эффектов этц) тупо в наш контекстf
. ИзInt
получаетсяParser Int
— который ничего не парсит, ни единого символа, а просто возвращает переданный инт. Добавим монаду — будетreturn = pure
, то есть это просто аппликативный ретурн.Вторая и последняя примочка Applicative — это способ апплаить функции тама, в контексте, с эффектами, над эффектными аргументами.
Частично применяя, можно двумя способами расставить скобки: либо так
f (a -> b) -> (f a -> f b)
— тогда мы «грязную» функцию как бы выдергиваем этим оператором оттуда на уровень обычной хаскельной функции; либо как бинарный оператор, принимающий грязную функцию, грязный аргумент, и отдающий грязный результат. Интуитивно?(*>)
и(<*)
— это частные случаи для удобства, они выбрасывают результат (но не эффекты) со стороны где не хватает треугольной скобки. по сигнатурам всё видноДальше, один из законов Applicative такой, что
f <$> x = pure f <*> x
. Если не шаришь функтор, то так легче понять(<$>)
; берёт чистую фукнцию, лифтит её туда и применяет к эффектной правой части. Вот там гдеVarDecl <$> foo <*> bar
именно это и происходит: чистая функция-конструктор применяется к результатам парсеровfoo
иbar
, эффекты автоматически делаются. УVarDecl
два аргумента, но по цепочке можно делать сколько хочешь — потому что типVarName -> Expr -> VarDecl
матчитa -> b
если положитьa = VarName, b = Expr -> VarDecl
.<|>
— это из Alternative, и это просто композиция по or. Либо то, либо следующее, проще простого.@ulidtko Да ты с ума сошёл, такие портянки без предварительного допроса писать! Я в курсе, что такое Applicative.
@minoru @anonymous не знает
// знаешь но не юзаешь? при парсинге? ЛОЛ
@ulidtko > @anonymous не знает
Но мы с тобой одни в этом треде ._.
Сорь. Хотел сейчас сослаться на то, что меня испортила документация к парсеку, глядь — а там везде тоже все эти
<*>
,<$>
и<|>
. Сфигали я всегда думал, что «парсер-комбинаторы» обязательно подразумевают монадки — хз.Спасибо, что просветил!
@ulidtko Олсо в аппликативном стиле из-за его сжатости как-то стыдно писать
<?>
, а без этого дебаг парсера превращается в ад.@ulidtko Переписал часть кода, сделал вывод, что для простых кусочков аппликативный стиль действительно получше, но как только появляются сложные опциональные элементы, наступает жопа. При этом эти же сложные части в монадном стиле пишутся легко и непринуждённо.
@minoru хули тебе там стыдно // один
<?>
на нетерминал, аккуратно в конце выражения, последней строкой@ulidtko Стыдно потому, что был однострочник, а стал многострочник. Твой совет касательно
<?>
плохо работает со штуками типа(keyword "BEGIN" *> parseBlock <* keyword "END")
, где хотелось бы отдельно пометить кейворды (у меня парсер на байтстроках работает, я включилOverloadedStrings
, а<?>
принимаетString
— я думал, но пока что не осилил, добавить<?>
в само определениеkeyword
; это облегчает, но не убирает проблему).@minoru
жопой чую, что у тебя там ещё и AST кривое. Вопрос! Чему вот здесь
будет равно
value
, если в тексте инициализация пропущена? как факт неинициализированности сохраняется в AST? умеет ли AST отличатьvar foo;
отvar foo = ();
иvar foo = Nothing;
?Я к чему веду, даже с фиксированной грамматикой есть уйма способов написать разные, но эквивалентные парсеры к ней. Также можно эквивалентно преобразовывать саму грамматику (не меняя её языка). То, что грамматика цитирует
[ = Expr]_Opt
, не значит что её парсер надо всенепременно записывать черезoptional
; для твоего AST наверняка будет лучше развернуть обе альтернативы через<|>
(получив эквивалентный парсер), и уложить их в разные конструкторы. Хотя бы просто чтобы сделать невалидные состояния непредставимыми.Так-то я бы
optional
не задумываясь использовал только наParser ()
, типаoptional whitespace
. Везде где внутренний парсер возвращает какие-то данные — вместоoptional
разворачивать альтернативы через<|>
. При появлении дубликации выносить парсеры в функции.Олсо, без конкретного кода мой уютный диван прогибается, на глазах дряхлеет и начинает чем-то пахнуть.
@minoru
kwBegin :: Parser ()
kwBegin = keyword "BEGIN" <?> "BEGIN keyword"
бля, ну серьёзно, неужели всё так плохо с абстракцией?
@ulidtko С абстракцией всё збс, но то, что ты написал выше — бойлерплейт. Я хотел
keyword k = string k *> skipWhitespace *> pure () <?> k
, но обломился из-за вышеописанного расхождения в типах строки.@minoru бля лол, а
<?> unpack k
уже всё, слишком сложно штоле?@ulidtko AST у меня действительно кривенький, потому что тупо слизан с грамматики (с минимальными правками). Если инициализация пропущена, то в
value
будетNothing
, фигли тебе не ясно? Да, все три показанных тобой случая отличаются в AST-е (будетNothing
,Just ()
иJust Nothing
).@ulidtko Ага.
unpack
внезапно выдал[Word8]
. Та-да-а!@minoru
понятно.
@ulidtko Не-не-не, это не тот
Nothing
, которыйData.Maybe.Nothing
, это другой, из AST. Честно ._.@minoru сука блядь, ненавижу эту Lazy/Strict хуйню, пидарасы блядь
@ulidtko Спасибо, я погляжу попозже. Меня настораживает, что оно unsafe. Ты меня куда ведёшь, дьявол?!
@minoru юникод в
keyword
не суй — и всё будет хорошо ;)@ulidtko или так
Другой широко известный бонус аппликативного парсинга — парсеры можно анализировать, компилировать (как регекспы), оптимизировать и иначе трансформить перед началом парсинга.
uu-parsinglib
, например, подсчитывает сам глубину look-ahead-а, опираясь на структуру парсера. Подобные трюки невозможны с монадическими парсерами — об их структуре очень сложно что-то полезное сказать не запуская сам парсер. (потому что он легко может использовать промежуточные данные парсинга и/или инспектировать входной текст, решая каким комбинатором парсить дальше).кароч если уж парсить — то аппликативно
@ndtimofeev пруф или везде
@ndtimofeev тупой штоле,
словом, точно так же как парсил бы пачку языковых конструкций, где сначала идёт кейворд определяющий структуру конструкции.
сорь, я прост криво сформулировал, отличие монадных парсеров от аппликативных немного глубже. справа от >>= у нас уже в скоупе промежуточный результат
r
парсера слева — и любой код, подставляющий произвольный парсер зависимо отr
, допусти́м справа. в аппликативе такая хуйня не прокатит, структура парсера фиксирована и не может динамически варьировать в рантайме. типа (может быть) не получится сделать аппликативно парсер протокола, который внутри себя может на лету описывать свои же расширения (и тут же их использовать в следующем меседже). но я хз, никогда не пробовал такое.но все(?) практичные парсеры имеют статичную грамматику, так что на деле это «ограничение» аппликатива никого не ебёт. ЗАТО оно покупает возможность анализа и оптимизации парсеров без их запуска.