Skip to content

ParserBase Reference

ParserBase.hs is the engine underneath the course's parser library. It defines the Parser type and the handful of primitives everything else is built from. The friendlier toolkit on top of it is covered in the Parser Combinator Reference.

You almost never import ParserBase yourself. import ParserCombinators already re-exports its essential pieces. This page is here for completeness: it lists what ParserBase provides, the few extras you'd need to import it directly for, and (if you're curious) how the parser type works under the hood. You don't need to understand its implementation, which uses some Haskell features we haven't covered.

Credit

The ParserBase 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.

What ParserCombinators Already Gives You

These ParserBase names are re-exported by ParserCombinators, so a plain import ParserCombinators is enough to use them. Each is documented in the Parser Combinator Reference.

Name Think of the type as What it does
Parser a A parser that produces a result of type a
get Parser Char Reads any single character
pfail Parser a Always fails
parse Parser a -> String -> a Runs a parser on a whole string
parseFile Parser a -> String -> IO a Runs a parser on the contents of a file
<|> Parser a -> Parser a -> Parser a Tries one parser, or else another
many Parser a -> Parser [a] Zero or more matches
some Parser a -> Parser [a] One or more matches

Extras That Need import ParserBase

ParserCombinators doesn't re-export these four, so to use them, import them from ParserBase alongside the usual import:

import ParserCombinators
import ParserBase (eof, succeeding, (<||>), parseNamed)

eof

eof :: Parser ()

Succeeds (producing ()) only if there is no input left. parse already requires a parser to consume its whole input, so you rarely need eof at the top level, but it can be handy in the middle of a larger parser.

ghci> parse (number <+-> eof) "42"
42
ghci> parse (get <+-> eof) "42"
*** Exception: <input>:1:2 -- EOF expected

succeeding

succeeding :: a -> Parser a -> Parser a

succeeding x p tries p. If p fails, it succeeds anyway, reads nothing, and produces the fallback value x.

ghci> parse (succeeding 0 number) "7"
7
ghci> parse (succeeding 0 number) ""
0

It behaves almost exactly like p <|> return x. The difference is that succeeding is guaranteed to succeed, which Haskell can take advantage of when parsing lazily.

<||>

(<||>) :: Parser a -> Parser a -> Parser a     -- infixl 3

Like <|>, it succeeds if either parser succeeds. The difference is in the error message when both fail: <||> always reports the error from the second parser, throwing away whatever the first one found. The <??> operator is built on it. See How Errors Are Chosen below for an example.

parseNamed

parseNamed :: Parser a -> String -> String -> a

parseNamed p name input is parse with a name of your choosing in error messages instead of <input>. (parseFile uses it to put the file name in its errors.)

ghci> parseNamed number "config.txt" "12x"
*** Exception: config.txt:1:3 -- Different kind of character expected

Error Positions

Every error message has the form name:line:column -- message. Lines are numbered from 1.

Columns after the first line are off by one

On the first line of the input, columns are numbered from 1. After a newline, though, get resets the column to 0, so on every later line the column number is one less than you'd expect. For example, a problem at the second character of line 2 is reported as 2:1.

Under the Hood

Everything from here down is optional. It's useful context if you want to connect the library to the parsers you build yourself in Module 6.

What a Parser Is

A Parser is a function wrapped in a type (the same idea as the parser functions you write by hand in class, with extra bookkeeping for error messages):

type ParsePosn  = (Int, Int)            -- (line, column)
type ParseInput = (String, ParsePosn)   -- the remaining input, and where it starts
type ParseError = (String, ParsePosn)   -- an error message, and where it happened

newtype Parser a = ParsingFunction (ParseError -> ParseInput -> ParseResult a)

data ParseStatus a = Success a ParseInput
                   | Failure ParseError

type ParseResult a = (ParseError, ParseStatus a)

In words: a parser takes the best error found so far and the input that's left to read. It produces an updated best error, plus either Success (with a result and whatever input is left over) or Failure (with an error).

newtype works like data with exactly one constructor holding exactly one thing. It lets Haskell treat Parser as its own type, which is what allows it to belong to type classes like Monad.

Where the Operations Come From

ParserBase makes Parser an instance of several standard Haskell type classes. Each instance supplies some of the operations you use, which is why their general types mention those classes:

Instance Provides Used for
Functor fmap >>=: and >>:
Applicative pure, <*> return, and all the sequencing operators (<+>, <+->, ...)
Monad >>= "and then"
MonadFail fail failing with a message
Alternative empty, <|>, many, some choice and repetition
MonadPlus mzero, mplus <=> (via mfilter) and perhaps (via join)

ParserBase also re-exports a few general-purpose Haskell functions that ParserCombinators is built from: empty (a parser that always fails, like pfail), mfilter, and join, along with the Alternative and MonadPlus classes themselves. You won't need them directly.

How Errors Are Chosen

When a parse fails, many alternatives have usually failed along the way. Which one should the error message describe? ParserBase's strategy is to report the error that got deepest into the input, on the theory that the parser that made the most progress is the one the author most likely meant.

That's what <|> does. <||> instead always reports the second parser's error. Here's the difference, with a parser that looks for abc or else xx, run on the input abd:

abc = char 'a' <-+> char 'b' <+> char 'c'
xx  = char 'x' <+> char 'x'
ghci> parse (abc <|> xx) "abd"
*** Exception: <input>:1:3 -- Expected 'c'
ghci> parse (abc <||> xx) "abd"
*** Exception: <input>:1:1 -- Expected 'x'

<|> points at column 3, where abc got stuck, which is the useful answer. <||> reports that there's no x at the start, which is true but not very helpful.

Backtracking

When the first parser in p <|> q fails, q starts over from the same place p did, no matter how much input p read before it failed. (Some parser libraries don't do this, and require you to mark the places where they should.) That means you can list alternatives that share a prefix without any special care:

ghci> parse ((string "ab" <+> string "c") <|> (string "a" <+> string "bd")) "abd"
("a","bd")

The trade-off is speed: a parser with many alternatives that share long prefixes can end up rereading the same input many times. For the languages in this course, that's never a problem.