Parser Combinators in Haskell
serokell.io
serokell.io
One aspect that I see discussed less often is the idea of 'builder combinators', which are kind of the inverse of parser combinators. There are multiple libraries in Haskell which make it very easy to write HTML/SVG/... this way:
https://hackage.haskell.org/package/blaze-html
https://hackage.haskell.org/package/blaze-svg
I haven't needed to do the same with SQL but I imagine that this already exists as well
`div :: [Attributes] -> [Html] -> Html`
Instead of using do notation to build up multiple children one would simply supply a list of children. Added benefit of using simpler map/filter/filterMap/mapMaybe vs their monadic counterparts
The builder interface indeed does not need to be monadic, you can simply append things using <> and get the same benefits.
The monadic interface is purely a syntactic enhancement on top of that, to reduce the number of operators you need.
But yeah. Mistakes were made.
[1]: https://github.com/inhabitedtype/httpaf
[2]: https://github.com/ocaml-multicore/retro-httpaf-bench/pull/1...
[3]: https://github.com/anmonteiro/ocaml-h2
It's also notable for being a continuous integration-driven talk!
For instance, in the book of Grune and Jacobs on parsing techniques, they are presented as essentially a curiosity (with 2.5 pages of description in a 600+ page book).
There is of course plenty of academic work specifically on how to implement parser combinator libraries efficiently, how to make them invertible (so they can also produce prettyprinters), provide good error messages, etc.
There isn't much to cover about them, they are just very ergonomic. Even guaranteeing a good performance is covered on the theory of other types of parser, that is them trivial to replicate on them, but it's not about them.
But really, otherwise there is not much to say about parser combinators, as they are just code. The cool thing about parsing is that usually you can give much nicer descriptions of the grammar than via parser combinators, and you can analyse these descriptions (uniqueness, for example), und generate optimised code.
Also, higher-kinded types are pretty much required to make parser combinators nice, so that restricts the appeal of parser combinators to just Haskell and a few other niche languages.
Yet another reason is that like all recursive descent parsing with backtracking, it's easy to accidentally make your parser run in exponential time. It certainly requires discipline and deliberation to avoid that.
†: I only know of one library in Haskell that has a parser-combinator-like interface without recursive descent parsing, and that's https://github.com/ollef/Earley but I'll quote from its README the difference between that and typical parser combinators:
> The grammar language is similar to that of many parser combinators (Parsec, Attoparsec, parallel parsing processes, etc.), providing an applicative interface, but the parser gracefully handles all finite CFGs, including those with left-recursion. On the other hand, its productions are not monadic meaning that it does not support context-sensitive or infinite grammars, which are supported by many parser combinator libraries.
And that's why parsing research doesn't pay much attention to it - it's an interface, not a different approach to handling data while parsing. It's all about ergonomics of using a parser, not about the capability and performance characteristics of parsing.
Should more research pay attention to that? Probably. BISON/YACC/ANTLR are all hell to use. Combinator-based libraries are easier, but current designs lack things like extending parsers without modifying them. There's a lot of research that could be done that would care about this aspect of parsers, but it doesn't seem to be for now.
It was the ‘(>>=) = Parser<‘a> -> (‘a -> Parser<‘b>) -> Parser<‘b>’ that flicked the lightswitch in my head.
void Structure(IVisitor visitor);
a good function signature, instead of: A Structure(IParser<A> parser);
I mean fair enough that you can't be bothered to implement the functor and monad methods, but it's downright silly to define a function that is incapable of returning output. public sealed class Unit {
private static Unit m_value = new Unit();
public static Unit Value => m_value;
public Unit() { }
public override string ToString() => "()";
public override bool Equals(object obj) => obj is Unit;
public override int GetHashCode(object obj) => 0;
}
and then using it ("return Unit.Value") in place of void is really not that big a problem.Combinators in general are concise and practical ie. runtime type assertions [1] or generators [2] etc.
We use them in several places in production.
[0] https://github.com/preludejs/parser
Off-topic: I'd really like to get more and more Haskell features in Python. Playing with the adt library for algebraic data types, and I'd also like to find something to reproduce monads/applicatives/functors/arrows etc
p <*> (q <|> r) = (p <*> q) <|> (p <*> r)
or at least that these are equivalent, then you might want to step back and wonder how on earth distributivity got into it. And after thinking for a few seconds and realizing that the parser which parses the empty string and the parsers which parses nothing correspond to the neutral elements for <*> and <|> respectively you might start to suspect some things.Anyway personally I reckon the proper space for dealing with these questions is by treating the space of parsers as a semiring. Although for parser combinators you break the commutativity of addition, but I can't find a better name than semiring right now.
Basically you can show that if you've got something that sends the space of strings to a semiring then you can turn it into a linear map on the semimodule formed by simply multiplying strings and values of the semiring in the obvious way. You then get a semiring of parsers by using composition and addition of linear maps.
Parser generators as described in this article are essentially based on the semiring formed by functions with composition as multiplication and f + g = f as addition (note, not commutative). This is wrapped in an 'Either' which also seems to have some semiring structure that I can't be bothered to analyse right now, though I noticed that <*> and <|> translated pretty neatly.
Anyway all you have to do to get your Viterbi/HMM equivalent is to use the semiring formed by real numbers with 'max' as multiplication and ordinary multiplication as the addition operator.
See also 'Kleene algebra' for more information in this direction.