Parser Combinator Reference¶
Starting in Module 6, you'll write parsers by gluing together small parsers with parser combinators. The course's combinator library lives in two files that ship with every parsing lab and assignment: ParserBase.hs, which holds the core machinery, and ParserCombinators.hs, which builds a toolkit on top of it. You'll only ever need one line to use the library (the ParserBase Reference covers the engine underneath, if you're curious):
import ParserCombinators
This page covers everything that import gives you, grouped by what you'd use it for. Keep it open while you work. You don't need to memorize it, and you don't need to understand how the library is implemented: several definitions are written in a terse, library-heavy style that doesn't look like what we write in class.
Credit
The ParserCombinators library and its original documentation were written by the HMC CS 131 staff (Melissa O'Neill, Chris Stone, Ben Wiedermann, and Lucas Bang). This page is adapted from that documentation.
Reading the Types¶
Many of the library's functions have more general types than the ones we use in class. For example, if you ask ghci for the type of <|>, you get:
(<|>) :: Alternative f => f a -> f a -> f a
That's because the library reuses general Haskell features (the Functor, Applicative, Alternative, and Monad type classes) that Parser belongs to. Everywhere you see a type variable like f, m, p, or parser, read it as Parser:
(<|>) :: Parser a -> Parser a -> Parser a
This page always gives the "think of it as" version with Parser filled in.
At a Glance¶
| You want to... | Use |
|---|---|
| Run a parser on a string or file | parse, parseFile |
| Read one character (any, or of a certain kind) | get, char, digit, letter, alphanum, space, getCharThat |
| Match an exact string | string, text |
| Match a punctuation character, skipping whitespace first | sym, openparen, closeparen, openbrace, closebrace |
| Parse a name or a number | ident, identifier, num, number, double |
| Do one parser, then another | <+>, <+->, <-+>, <-+->, <:>, <++>, >>= |
| Try one parser, or else another | <|> |
| Change what a parser returns | >>=:, >>: |
| Keep a result only if it passes a test | <=> |
| Repeat a parser | many, some, many1, skipMany, skipMany1 |
| Parse something that might not be there | optional, perhaps |
| Parse something inside delimiters | parens, braces, between |
| Parse a list with separators or terminators | sepBy, sepBy1, endBy, endBy1 |
Parse operators like 1 - 2 - 3 |
chainl1, chainr1 |
| Deal with whitespace | whitespace, skipws |
| Give better error messages | <??>, <???> |
| Always succeed or always fail | return, pfail, fail |
How do you even say <-+-> out loud?
This library is mostly punctuation: <+->, <??>, >>=:, <=>... Most of us type characters like these all the time without ever learning what to call them, which makes pair programming (or asking a question in office hours) surprisingly awkward. Is < "less than," "left angle bracket," or something else entirely? Programmers settled the matter long ago, in verse: read Waka Waka Bang Splat. It only works if you read it aloud.
All of the examples below are real ghci sessions. When a parse fails, ghci also prints a few lines of CallStack after the error; those are left out here.
The Parser Type¶
data Parser a
A Parser a is something that can read an input string and, if it succeeds, produce a result of type a. A Parser Char produces a character, a Parser Integer produces a number, a Parser Expr might produce a whole abstract syntax tree.
Running a Parser¶
parse :: Parser a -> String -> a
parseFile :: Parser a -> String -> IO a
parse p input runs the parser p on input and returns the result. The parser has to match the entire input. Leftover characters count as an error:
ghci> parse number "42"
42
ghci> parse number "12 34"
*** Exception: <input>:1:3 -- Different kind of character expected
parseFile p path does the same thing, but reads the input from the file at path. Because it reads a file, its result is wrapped in IO.
Error messages point at the problem as line:column (column 3 above is the space before 34).
Primitive Parsers¶
These are the building blocks everything else is made from.
get :: Parser Char
Reads a single character, whatever it is. Fails only if there's no input left.
ghci> parse get "x"
'x'
ghci> parse get ""
*** Exception: <input>:1:1 -- Unexpected EOF
return :: a -> Parser a
Always succeeds, reads nothing, and produces the value you give it. It's handy as a "base case," e.g., "if nothing else matched, produce []."
pfail :: Parser a
fail :: String -> Parser a
Parsers that always fail. fail lets you choose the error message.
ghci> parse (fail "nope" :: Parser Int) "abc"
*** Exception: <input>:1:1 -- nope
Sequencing: This, Then That¶
Each of these runs parser p on the input, then runs q on whatever p left over. If either one fails, the whole thing fails. What differs is what they produce, and the shape of the operator tells you which result it keeps: the + marks the result you keep and the - marks the one you throw away.
| Combinator | Think of the type as | Produces |
|---|---|---|
p <+> q |
Parser a -> Parser b -> Parser (a, b) |
both results, as a pair |
p <+-> q |
Parser a -> Parser b -> Parser a |
only p's result |
p <-+> q |
Parser a -> Parser b -> Parser b |
only q's result |
p <-+-> q |
Parser a -> Parser b -> Parser () |
neither (just ()) |
p <:> q |
Parser a -> Parser [a] -> Parser [a] |
p's result consed onto q's list |
p <++> q |
Parser [a] -> Parser [a] -> Parser [a] |
the two lists appended |
ghci> parse (get <+> get) "ab"
('a','b')
ghci> parse (get <+-> get) "ab"
'a'
ghci> parse (get <-+> get) "ab"
'b'
ghci> parse (get <-+-> get) "ab"
()
ghci> parse (digit <:> many letter) "1abc"
"1abc"
ghci> parse (string "ab" <++> string "cd") "abcd"
"abcd"
The "keep one side" versions are what you'll use to throw away punctuation. For example, char '(' <-+> number <+-> char ')' parses a parenthesized number and keeps just the number.
And Then: >>=¶
(>>=) :: Parser a -> (a -> Parser b) -> Parser b
The "and then" (or bind) operator. p >>= f runs p, hands its result to f, and then runs the parser f returns. Unlike the sequencing operators above, it lets the second parser depend on what the first one found. Here, the second parser looks for the same character that the first one read:
ghci> parse (get >>= \c -> char c) "zz"
'z'
ghci> parse (get >>= \c -> char c) "zy"
*** Exception: <input>:1:2 -- Expected 'z'
Alternatives: This, or Else That¶
(<|>) :: Parser a -> Parser a -> Parser a
p <|> q succeeds if either p or q succeeds. Both sides have to produce the same type.
ghci> parse (many (letter <|> digit)) "a1b2"
"a1b2"
Transforming Results¶
(>>=:) :: Parser a -> (a -> b) -> Parser b
(>>:) :: Parser a -> b -> Parser b
p >>=: f runs p and passes its result through the ordinary function f. p >>: v runs p, ignores its result, and produces v instead. These are how you turn raw text into the values (and eventually AST nodes) you actually want.
ghci> parse (num >>=: (*2)) "21"
42
ghci> parse (text "yes" >>: True) " yes"
True
>>=: pairs naturally with <+>: parse two things, then combine the pair with a lambda. That's exactly how the library defines ident:
ident = letter <+> many alphanum >>=: \(l, ls) -> l:ls
Filtering Results¶
(<=>) :: Parser a -> (a -> Bool) -> Parser a
p <=> pred runs p and keeps the result only if it satisfies pred. Otherwise it fails.
ghci> parse (get <=> (== 'x')) "x"
'x'
ghci> parse (get <=> (== 'x')) "y"
*** Exception: <input>:1:2 -- No parse (via pfail)
That error message isn't very helpful, which is why the library also provides char, below.
Single Characters¶
getCharThat :: (Char -> Bool) -> Parser Char
digit :: Parser Char
letter :: Parser Char
alphanum :: Parser Char
space :: Parser Char
char :: Char -> Parser Char
| Parser | Reads one character that is... |
|---|---|
getCharThat pred |
anything satisfying pred |
digit |
a digit, 0–9 |
letter |
a letter, upper- or lowercase |
alphanum |
a letter or a digit |
space |
whitespace (space, tab, newline, etc.) |
char c |
exactly c |
ghci> parse (some (getCharThat (`elem` "aeiou"))) "aei"
"aei"
ghci> parse (char 'x') "y"
*** Exception: <input>:1:1 -- Expected 'x'
Compare that last error with the get <=> (== 'x') version in the previous section. char does the same job but adds a clearer message.
Strings¶
string :: String -> Parser String
text :: String -> Parser String
litstring :: Parser String
string s matches exactly the characters of s, next in the input. text s does the same but skips any whitespace before it first.
ghci> parse (string "hello") "hello"
"hello"
ghci> parse (text "hello") " hello"
"hello"
litstring parses a double-quoted string literal (skipping any whitespace before it) and produces what's between the quotes:
ghci> parse litstring " \"hello world\""
"hello world"
No escaped quotes
The library's documentation says litstring understands the escape \" for a quote inside the string, but it doesn't: the backslash is read as an ordinary character, and the quote right after it ends the string. Stick to string literals without embedded quotes. (stringchar :: Parser Char, the one-character helper litstring is built from, is also exported, and has the same limitation.)
Symbols and Delimiters¶
sym :: Char -> Parser Char
openparen :: Parser Char
closeparen :: Parser Char
openbrace :: Parser Char
closebrace :: Parser Char
parens :: Parser a -> Parser a
braces :: Parser a -> Parser a
between :: Parser open -> Parser close -> Parser a -> Parser a
sym c is like char c, but it skips whitespace before the character. openparen, closeparen, openbrace, and closebrace are just sym '(', sym ')', sym '{', and sym '}'.
parens p parses a p wrapped in parentheses and keeps only p's result. braces p does the same with curly braces. Both skip whitespace before each delimiter.
ghci> parse (parens number) "( 42 )"
42
ghci> parse (braces (identifier `sepBy` sym ',')) "{a, b, c}"
["a","b","c"]
between open close p is the general version, for whatever delimiters you like. Note the argument order: the delimiters come first, then the thing inside them.
ghci> parse (between (char '<') (char '>') ident) "<tag>"
"tag"
Whitespace¶
whitespace :: Parser ()
skipws :: Parser a -> Parser a
whitespace consumes zero or more whitespace characters and produces (). skipws p skips any leading whitespace, then runs p.
ghci> parse (whitespace <-+> get) " z"
'z'
Leading vs. trailing whitespace
The library's convention is to skip whitespace before each token, never after. That's why text, sym, identifier, number, and friends all skip leading whitespace. If your input might end with trailing spaces or a newline, finish your top-level parser with <+-> whitespace so that parse doesn't complain about leftover input.
Identifiers and Numbers¶
ident :: Parser String
identifier :: Parser String
num :: Parser Integer
number :: Parser Integer
double :: Parser Double
ident parses an identifier: a letter followed by zero or more letters and digits. num parses an integer, with an optional leading -. identifier and number are the same, but skip leading whitespace first. Most of the time, you want the whitespace-skipping versions.
double parses a decimal number, including scientific notation (like 3.14e2), skipping leading whitespace.
ghci> parse ident "x42"
"x42"
ghci> parse identifier " foo7"
"foo7"
ghci> parse ident "42x"
*** Exception: <input>:1:1 -- <ident> expected
ghci> parse number " -17"
-17
ghci> parse double " 3.14e2"
314.0
Repetition¶
many :: Parser a -> Parser [a]
some :: Parser a -> Parser [a]
many1 :: Parser a -> Parser [a]
skipMany :: Parser a -> Parser ()
skipMany1 :: Parser a -> Parser ()
many p matches p zero or more times and produces a list of the results. some p matches it one or more times. many1 is just another name for some. skipMany and skipMany1 are the same as many and some, but throw the results away.
ghci> parse (many digit) ""
""
ghci> parse (some digit) ""
*** Exception: <input>:1:1 -- Different kind of character expected
ghci> parse (skipMany (char 'a') <-+> get) "aaab"
'b'
many never fails
Because zero matches is fine, many p always succeeds. That makes it easy to write a parser that quietly matches nothing when you expected it to match something. If there has to be at least one, use some.
Optional Pieces¶
optional :: Parser a -> Parser (Maybe a)
optional :: Parser a -> Parser [a]
perhaps :: Parser [a] -> Parser [a]
optional p tries to parse a p, but still succeeds if there isn't one. It can produce its result as either a Maybe or a list, whichever your code needs; Haskell works out which from the context:
ghci> parse (optional (char '-')) "" :: Maybe Char
Nothing
ghci> parse (optional (char '-')) "-" :: String
"-"
perhaps p is for parsers whose result already has an "empty" value, like a String (""), a list ([]), or a Maybe (Nothing). If p doesn't match, perhaps p produces that empty value.
ghci> parse (perhaps (string "abc")) ""
""
Lists with Separators¶
sepBy :: Parser a -> Parser sep -> Parser [a]
sepBy1 :: Parser a -> Parser sep -> Parser [a]
endBy :: Parser a -> Parser sep -> Parser [a]
endBy1 :: Parser a -> Parser sep -> Parser [a]
p `sepBy` sep parses zero or more ps with a sep between each pair, like 1, 2, 3. endBy is similar, but with a sep after every p, like 1; 2;. In both cases the separators are thrown away. The 1 versions require at least one p.
ghci> parse (number `sepBy` sym ',') "1, 2 ,3"
[1,2,3]
ghci> parse (number `sepBy` sym ',') ""
[]
ghci> parse (number `endBy` sym ';') "1; 2;"
[1,2]
The library also has manyEndingWith end p and someEndingWith end p, which read ps until they reach an end. In practice, many p <+-> end does the same job, so you shouldn't need them.
Chains of Operators¶
chainl1 :: Parser a -> Parser (a -> a -> a) -> Parser a
chainr1 :: Parser a -> Parser (a -> a -> a) -> Parser a
chainl1 p op parses one or more ps separated by ops, where each op produces a function that combines two results. It then combines them, grouping from the left, like foldl. chainr1 does the same, but groups from the right, like foldr. The difference matters for operators like subtraction:
ghci> parse (chainl1 number (sym '-' >>: (-))) "10 - 3 - 2"
5
ghci> parse (chainr1 number (sym '-' >>: (-))) "10 - 3 - 2"
9
The first computes (10 - 3) - 2, the second 10 - (3 - 2). Most arithmetic operators are left-associative, so chainl1 is usually what you want.
Putting It Together: Arithmetic¶
These combinators are enough to write a complete parser for arithmetic expressions that produces an AST with the correct precedence (* before +) and handles parentheses:
import ParserCombinators
data Expr = Num Integer
| Plus Expr Expr
| Times Expr Expr
deriving Show
expr, term, factor :: Parser Expr
expr = chainl1 term (sym '+' >>: Plus)
term = chainl1 factor (sym '*' >>: Times)
factor = (number >>=: Num) <|> parens expr
ghci> parse expr "1 + 2 * (3 + 4)"
Plus (Num 1) (Times (Num 2) (Plus (Num 3) (Num 4)))
Each level of the grammar gets its own parser. Constructors like Plus and Times are already two-argument functions, which is exactly what chainl1 needs from its operator parser.
Error Messages¶
(<??>) :: Parser a -> String -> Parser a
(<???>) :: Parser a -> String -> Parser a
Both attach your own error message to a parser, to replace the default one when it fails. They differ in how aggressive they are:
p <??> msgalways replacesp's error withmsg, throwing away any information about how farpgot.p <???> msgreplaces the error only ifpgot nowhere (failed on its very first character). Ifpmade some progress before failing, its own, more specific error message is kept.
ab = char 'a' <+> char 'b'
ghci> parse (ab <??> "an ab pair expected") "ax"
*** Exception: <input>:1:1 -- an ab pair expected
ghci> parse (ab <???> "an ab pair expected") "ax"
*** Exception: <input>:1:2 -- Expected 'b'
ghci> parse (ab <???> "an ab pair expected") "zz"
*** Exception: <input>:1:1 -- an ab pair expected
Operator Precedence¶
When you mix operators without parentheses, these fixities decide how they group. A higher number binds more tightly.
| Operators | Fixity |
|---|---|
<=> |
infix 7 |
<+>, <+->, <-+>, <-+-> |
infixl 6 |
<:>, <++> |
infixr 5 |
<|>, <??>, <???> |
infixl 3 |
>>=:, >>:, >>= |
infixl 1 |
In practice, that means two things:
- Transform last.
p <+> q >>=: fmeans(p <+> q) >>=: f, so you can sequence several parsers and then transform the combined result, with no parentheses. - Parenthesize alternatives that each transform.
a >>=: f <|> b >>=: gdoes not do what it looks like. Write(a >>=: f) <|> (b >>=: g), as infactorabove.