What the heck is a parser-combinator?
kimpel.com
kimpel.com
string("foobar") : Parser<String>
char : Parser<Character>
always<A>(x: A): Parser<A>
never<A>: Parser<A>
These are parser combinators. They form the atoms at the foundation of a language for constructing parsers. For instance, we can imagine two parsers happening in sequence then<A, B>(a: Parser<A>, b: Parser<B>): Parser<(A, B)>
then(char, char): Parser<(Character, Character)>
Or more fancily, a two parsers happening in sequence, but the second parser being defined using the output of the first. This creates context sensitivity thenDependent<A, B>(a: Parser<A>, b: A -> Parser<B>): Parser<(A, B)>
We're constructing a fundamental language for building parsers up from our atoms to larger and larger things. All we need now is a way to "run" them run<A>(p: Parser<A>): String -> Option<A>
Tada---parser combinators!There is a nice paper on the subject by Graham Hutton and Erik Meijer. It's a very good introduction to both parser combinators and monads, and it's very readable even for beginners.
type Parser t = Input -> Maybe (t, Input)
or similar would be enough? Simple function composition gets you the rest of the way. (I grant you, it might be a bit tedious to write parsers this way, and monadic notation certainly makes it more pleasant in most cases.)For anyone following along at home: think function which takes input + current position and may return: "Nothing" if the parser doesn't match, or "a value of type t along with the rest of the unparsed input".
(Obviously it'd have to be expanded to support errors properly, etc.)
EDIT: Of course, I guess you could argue that they just are monadic by definition since chaining using function composition and parameter passing is the essence of a monad... Maybe that's what you were driving at?
Except the newtype -- which was kind of my point :).
always<A>(x: A): Parser<A>
thenDependent<A, B>(a: Parser<A>, b: A -> Parser<B>): Parser<(A, B)>
as: always<A>(x: A): Parser<A>
thenDependent<A, B>(a: Parser<A>, b: A -> Parser<B>): Parser<B>
which would make a monad provided the monad laws are satisfied.Of course you don't have to write parser combinators that way, but I found it pretty practical compared to dealing with nested tuples for the sequence parser. Check out Hutton and Meijer's article for more details.
string "foo" <|> string "for"
It would be nice to automatically rewrite this as string "fo" >> (char 'o' <|> char 'r')
But if we try to analyze monadic parsers like that we run into the halting problem.Applicative parsers are less powerful so we can left factor them automatically. There were some attempts to combine applicative and monadic parsers which resulted in arrow syntax but that kind of feels like the worst of both worlds for many cases.
FastParse lets you use parser combinators monadically, but that's only necessary in uncommon cases. The three I've come across are length + data-of-length constructs (more common in binary parsing than text), having your parser validate matched XML tags/closing-tags, and indentation-delimited-block parser (e.g. when parsing Python).
For the vast majority of programming-language-like things, Applicative parser combinators are enough, including languages with complex syntax such as Scala.
I suspect these combinators are not very powerful compared to e.g. LR(k) parsers, and provide a false sense of modularity. (E.g. a minor grammar change leading to a large scale rewrite).
Further, do monads warn the user when there is an ambiguity in the "grammar"?
Monadic parsers can do context-sensitive things that context free parsers can't, but they can't have unordered options which context free parsers can. So really monadic parsers have different powers to context-free.
> do monads warn the user when there is an ambiguity in the "grammar"?
You don't have ambiguity in the grammar because monadic parsers only provide ordered options.
Oh, but you do have ambiguity in the grammar, except that parser returns one of the possible parse trees deterministically and thus doesn't warn you that the input could be parsed differently. This leads to the false impression that your grammar is unambiguous.
Specifically, it makes it impossible for you to even write down that ambiguous grammar you wanted.
I'm not aware of a parser combinator library that does this. The way you build parser combinators in Haskell doesn't exactly preclude it from being done but it wouldn't be totally trivial either.
Googling for it didn't turn up much that wasn't a veiled reference to this paper:
http://richard.myweb.cs.uwindsor.ca/PUBLICATIONS/PADL_08.pdf
Context sensitivity as I wrote it here is the most powerful but least optimizable version. There are plenty of tricks to improve optimization. You can even write a "cut" combinatory to break backtracking.
It's a huge design space. The answer to all of your questions will probably be: it depends.
No need to create an account or login
For similar reasons, too, I think PEG-style grammars are more often conceptually closer than BNF grammars for early learning/thinking in parser-combinators. But again a good parser-combinator library will have power beyond what you might consider the possibility space afforded by just PEG-style grammars, especially as you start to get into higher order combinators.
Do parser combinators have the notion of arbitrary lookahead or backtracking?
string :: String -> Parser String
string s = mapM char s
On the other hand this probably degrades from c-like performance to python levels so some higher level builtins that are easy to optimize and compile into efficient assembly might be a better idea.Just like I'd call '+' and '*' arithmetic operators but I'd call '3' a number.
"Combinator" != "combiner". A combinator is a thing which is combined with other combinators. When you combine two combinators, you end up with yet another combinator which can be combined with other combinators. Very composable, in the functional spirit of things.
Also, in theory a combinator is just an expression of no free variables. S-K-I calculus is the canonical set of "combinators" and all three of them could be considered "functions" but also could be considered "atoms". So perhaps it's just vague.
PEGTL - C++ Parsing Expression Grammar Template Library https://github.com/taocpp/PEGTL
Parboiled - Java & Scala PEG Library https://github.com/sirthias/parboiled
Nom - Rust parser combinator framework https://github.com/Geal/nom
Nearley - JavaScript parser toolkit https://github.com/Hardmath123/nearley
Neotoma - Erlang library and packrat parser-generator for PEGs https://github.com/seancribbs/neotoma
There's a great walk-through of using it here, including fuzzing your parsers to make sure they're solid. https://github.com/Geal/langsec-2017-hackathon-code
The library has a well-written tutorial that doesn't require previous knowledge about parser combinators.
check out Chevrotain
I used that 2 times and found it pretty easy to use, once I had grasped all the operators (like .>>, .>>., <|>, etc.)
The weird thing is that all people have to do to make this stuff work is: nothing at all. But for some reason that's just too much effort.
No smooth scroll js. In fact, I read the whole article and never even realized there was any smooth scroll js anywhere.
local lpeg = require"lpeg"
local P, S, R, V = lpeg.P, lpeg.S, lpeg.R, lpeg.V
local C, Cc, Cf, Cg, Ct = lpeg.C, lpeg.Cc, lpeg.Cf, lpeg.Cg, lpeg.Ct
local function to8(n)
... -- Lua code to normalize UTF-16 to UTF-8
end
local unicode = P"u" * (R("09", "AF", "af")^4 / to8)
local named = C'"' + C"\\" + C"/" + (P"b" * Cc"\b") + (P"f" * Cc"\f") + (P"n" * Cc"\n") + (P"r" * Cc"\r") + (P"t" * Cc"\t")
local escaped = P"\\" * (named + unicode)
local unescaped = C((P(1) - S'\\"')^1)
local qstring = Ct(P'"' * (unescaped + escaped)^0 * P'"') / table.concat
local exp = S"Ee" * S"-+"^-1 * R"09"^1
local frac = P"." * R"09"^1
local number = (S"-+"^-1 * R"09"^1 * frac^-1 * exp^-1) / tonumber
local boolean = (P"true" * Cc(true)) + (P"false" * Cc(false))
local null = P"null" * Cc(nil)
local space = S" \t\r\n"^0
local JSON = { "Value",
Value = space * (V"Object" + V"Array" + V"Simple") * space,
Object = Cf(Ct"{" * space * Cg(qstring * space * P":" * V"Value" * P","^-1 * space)^0 * P"}", rawset),
Array = Ct(P"[" * space * (V"Value" * P","^-1 * space)^0 * P"]"),
Simple = number + boolean + null + qstring,
}
local function decode(txt)
return lpeg.match(JSON, txt)
end "A Parser for Things
Is a function from Strings
To Lists of Pairs
Of Things and Strings!"
- Fritz Ruehrhttps://github.com/JohnEarnest/ok/blob/gh-pages/examples/par...
Not particularly efficient, but fairly concise.
You've linked to my csharp-monad library for C# parser combinators. This has been superseded by my language-ext project: https://github.com/louthy/language-ext/
It is a much more advanced and efficient port of the Haskell Parsec library. Would you mind linking to that instead?
Your Sprache examples would look like this in language-ext:
public static readonly Parser<JCLCommand> JCLText =
from open in ch('$')
from ws1 in spaces
from command in asString(many(noneOf(' ')))
from ws2 in spaces
from content in asString(many(noneOf('"')))
select new JCLCommand(command, content);
public static readonly Parser<JCLCommand> GlobalText =
from variablename in asString(many(noneOf('=')))
from ws2 in ch('=')
from openbrack in ch('(')
from filepath in asString(many(noneOf(')')))
from closebrack in ch(')')
select new JCLCommand(variablename, filepath);
But the power of parser combinators are their reusable nature. So I'd break that down to a set of tools: static Parser<A> token<A>(Parser<A> p) =>
from x in p
from _ in either(spaces, eof)
select x;
static Parser<string> symbol(string x) =>
token(str(x));
static Parser<string> identifier =
token(asString(many1(alphaNum)));
static Parser<A> quotes<A>(Parser<A> p) =>
between(symbol("\""), symbol("\""), p);
static Parser<A> parens<A>(Parser<A> p) =>
between(symbol("("), symbol(")"), p);
static Parser<string> quoteText =
token(quotes(asString(many(satisfy(x => x != '"')))));
static Parser<string> parensText =
token(parens(asString(many(satisfy(x => x != ')')))));
Then your final parsers would look like this: static readonly Parser<JCLCommand> JCLText =
from open in symbol("$")
from command in identifier
from content in quoteText
select new JCLCommand(command, content);
static readonly Parser<JCLCommand> GlobalText =
from variablename in identifier
from ws2 in symbol("=")
from filepath in parensText
select new JCLCommand(variablename, filepath);
static readonly Parser<Seq<JCLCommand>> Commands =
from _ in spaces
from commands in many1(either(JCLText, GlobalText))
select commands;
Which is much easier to understand I think. It's not exactly the same as it defines what an identifier is, but it's much more tolerant of rogue spaces because of the token parser. This is definitely the most compelling aspect of parser combinators for me, the way they compose so elegantly.That's not a problem with parsers or parser-combinators though. That's a problem with converting from one DB to another.
The article is about converting JCL to PowerShell, so I'd say it's related.
I had a similar problem a few months ago with a Golang application that uses Postgres in production, but SQLite for unit tests. Since Go's SQL support has pluggable driver backends, I made a generic proxy driver [1] that can rewrite the incoming query, and used that in my application to rewrite from Postgres to SQLite syntax [2].
[1] https://godoc.org/github.com/majewsky/sqlproxy
[2] https://github.com/sapcc/limes/blob/205f9980a41d75fc0315e0ac...