Parser combinator
A parser is a function from input to a result and the rest of the input, or failure. Parser combinators build parsers from smaller ones: sequencing, choice, repetition. A grammar becomes a set of ordinary recursive definitions in the host language.
Sequencing, , runs and then the parser that builds from its result, on the remaining input. Choice, , tries where fails. Everything else is defined from those two and .
A parser for arithmetic, with the usual precedence and left associativity. Positions stand in for the rest of the input.
(* A parser takes the input and a position, and either fails or returns avalue and the position after it. *)type 'a parser = string -> int -> ('a * int) optionlet return x : 'a parser = fun _ i -> Some (x, i)let ( >>= ) (p : 'a parser) (f : 'a -> 'b parser) : 'b parser =fun s i -> match p s i with Some (x, j) -> f x s j | None -> Nonelet ( <|> ) (p : 'a parser) (q : 'a parser) : 'a parser =fun s i -> match p s i with Some _ as r -> r | None -> q s ilet ( let* ) = ( >>= )let satisfy pred : char parser =fun s i -> if i < String.length s && pred s.[i] then Some (s.[i], i + 1) else Nonelet char c = satisfy (( = ) c)let rec many p = (let* x = p in let* xs = many p in return (x :: xs)) <|> return []let many1 p = let* x = p in let* xs = many p in return (x :: xs)let number =let* ds = many1 (satisfy (function '0' .. '9' -> true | _ -> false)) inreturn (int_of_string (String.of_seq (List.to_seq ds)))(* Left-associative chains without left recursion: parse one operand, thenfold every (operator, operand) that follows onto it from the left. *)let chainl1 p op =let rec rest acc = (let* f = op in let* y = p in rest (f acc y)) <|> return acc inlet* x = p inrest xlet rec expr s i = chainl1 term (char '+' >>= (fun _ -> return ( + )) <|> (char '-' >>= fun _ -> return ( - ))) s iand term s i = chainl1 factor (char '*' >>= (fun _ -> return ( * )) <|> (char '/' >>= fun _ -> return ( / ))) s iand factor s i = (number <|> (let* _ = char '(' in let* e = expr in let* _ = char ')' in return e)) s ilet parse s = match expr s 0 with Some (v, j) when j = String.length s -> Some v | _ -> None
Running it.
1+2*3 -> 78-3-2 -> 3(1+2)*3 -> 9100/10/5 -> 22*(3+4)-1 -> 131+ -> no parse
The textbook grammar cannot be written directly: expr would call itself at the same position before consuming anything, and never return (in OCaml, a stack overflow). chainl1 parses the same language by reading one operand and then folding each following operator and operand onto it from the left, which is why 8-3-2 is 3 and not 7.
here commits to the first alternative that succeeds, as in a PEG. Returning a list of successes instead of an option gives every parse, which handles ambiguous grammars at exponential worst-case cost. Parsec commits as soon as an alternative has consumed input, which gives precise error positions and linear time on LL(1)-style grammars, with try to ask for backtracking explicitly.
Because can choose the next parser from a value already parsed, monadic combinators are context-sensitive: a length field followed by that many bytes is one line. Applicative combinators give that up, and in exchange a parser built from them can be inspected before it runs.
see also
- LR parsingBottom-up parsing driven by a table of states: the parser shifts tokens onto a stack, and reduces the top of the stack by a grammar rule when the next token says to. A grammar for which the table cannot be built without a choice has conflicts, reported as shift/reduce or reduce/reduce.
- ApplicativeA functor with pure : a -> f a and a way to combine independent computations, <*> : f (a -> b) -> f a -> f b, or equivalently product : f a -> f b -> f (a * b). Every monad is applicative, but applicatives are strictly more general: because later steps cannot depend on earlier results, effects can be accumulated, parallelised or inspected before running.
- MonadIn functional programming, a monad is a type constructor m with two operations, return : a -> m a and bind : m a -> (a -> m b) -> m b, satisfying three laws. It lets code with some extra behaviour, such as failure, several results, configuration, state or I/O, be written as a sequence of ordinary steps, with the behaviour defined once in bind. The notion comes from category theory.
further reading
- [1]P. Wadler, “How to replace failure by a list of successes”, FPCA (1985).
- [2]G. Hutton, “Higher-order functions for parsing”, Journal of Functional Programming 2 (1992).
- [3]G. Hutton, E. Meijer, “Monadic parsing in Haskell”, Journal of Functional Programming 8 (1998).
- [4]D. Leijen, E. Meijer, “Parsec: direct style monadic parser combinators for the real world”, Technical Report UU-CS-2001-27, Utrecht University (2001).