Parsing with Megaparsec
| A parser is a value | |
| Combinators | |
| Running a parser | |
| Labels and errors |
A parser is a value
A parser is a function from text to a result plus the rest of the input, wrapped in a type that also carries an error. Megaparsec gives that type a name and a set of combinators for building bigger parsers out of small ones:
import Text.Megaparsec
import Text.Megaparsec.Char
import Data.Void (Void)
type Parser = Parsec Void String
Void means this parser never produces a custom error type, and String is the input type. A Text input is faster, and the code below is otherwise identical.
Combinators
Six combinators build most grammars. Each one consumes input on success and leaves it untouched on failure, which is what makes them compose:
char 'x' -- one specific character
string "let" -- a literal string
some digitChar -- one or more digits
many letterChar -- zero or more letters
sepBy p (char ',') -- p, separated by commas
eof -- nothing left to read
some fails on an empty match and many succeeds with an empty list, so the choice between them is a decision about whether an empty value is legal.
number :: Parser Int
number = read <$> some digitChar
csvLine :: Parser [String]
csvLine = sepBy (some (noneOf ",\n")) (char ',')
noneOf accepts any character except the ones in the string. That single line parses a CSV row, and adding endBy over newlines parses the whole file.
Running a parser
parse takes a parser, a name for the source, and the input. The name appears in error messages, so use the real filename:
runNumber :: String -> Either String Int
runNumber s = case parse (number <* eof) "input" s of
Left err -> Left (errorBundlePretty err)
Right n -> Right n
The <* eof is not optional in practice. Without it, parse number "12abc" succeeds with 12 and silently ignores the rest, which is a bug that survives testing.
ghci> runNumber "12" Right 12 ghci> runNumber "12abc" Left "input:1:3:\n |\n1 | 12abc\n | ^\nunexpected 'a'\n"
The error points at the column where the parser stopped expecting more input, which is usually exactly where the problem is.
Labels and errors
A raw combinator error mentions characters. A label mentions the concept, and the message becomes something a user can act on:
identifier :: Parser String
identifier =
(some (alphaNumChar)) <?> "an identifier"
-- unexpected '!'
-- expecting an identifier
<?> replaces the expectations of the parser on its left when it fails without consuming input. Put it on every rule a user can trigger, and leave the internal helpers bare.
-- a tiny config grammar
setting = (,) <$> identifier
<*> symbol "="
<*> number
symbol s = string s <* space
A symbol helper that consumes trailing space is
the standard trick for making whitespace invisible in the grammar.
The capstone in the next lesson uses that idea for its command line.
Next:
Capstone: a todo CLI
Article author: Arthur Isaev