replace-megaparsec
replace-megaparsec is for finding text patterns, and also editing and
replacing the found patterns.
This activity is traditionally done with regular expressions,
but replace-megaparsec uses
megaparsec
parsers instead for the pattern matching.
replace-megaparsec can be used in the same sort of “pattern capture”
or “find all” situations in which one would use Python
re.findall
or
Perl m//
,
or
Unix grep
.
replace-megaparsec can be used in the same sort of “stream editing”
or “search-and-replace” situations in which one would use Python
re.sub
,
or
Perl s///
,
or Unix
sed
,
or
awk
.
See replace-attoparsec
for the
attoparsec
version.
Why would we want to do pattern matching and substitution with parsers instead of regular expressions?
-
Haskell parsers have a nicer syntax than
regular expressions,
which are notoriously
difficult to read.
-
Regular expressions can do “group capture” on sections of the matched
pattern, but they can only return stringy lists of the capture groups. Parsers
can construct typed data structures based on the capture groups, guaranteeing
no disagreement between the pattern rules and the rules that we're using
to build data structures based on the pattern matches.
For example, consider
scanning a string for numbers. A lot of different things can look like a number,
and can have leading plus or minus signs, or be in scientific notation, or
have commas, or whatever. If we try to parse all of the numbers out of a string
using regular expressions, then we have to make sure that the regular expression
and the string-to-number conversion function agree about exactly what is
and what isn't a numeric string. We can get into an awkward situation in which
the regular expression says it has found a numeric string but the
string-to-number conversion function fails. A typed parser will perform both
the pattern match and the conversion, so it will never be in that situation.
Parse, don't validate.
-
Regular expressions are only able to pattern-match
regular
grammers.
Megaparsec parsers are able pattern-match context-free grammers, and
even context-sensitive grammers, if needed. See below for
an example of lifting a Parser
into a State
monad for context-sensitive
pattern-matching.
-
The replacement expression for a traditional regular expression-based
substitution command is usually just a string template in which
the Nth “capture group” can be inserted with the syntax \N
. With
this library, instead of a template, we get
an editor
function which can perform any computation, including IO.
Usage Examples
Try the examples in ghci
by
running cabal v2-repl
in the replace-megaparsec/
root directory.
The examples depend on these imports.
import Data.Void
import Replace.Megaparsec
import Text.Megaparsec
import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer
Parsing with sepCap
family of parser combinators
The following examples show how to match a pattern to a string of text
and separate it into sections
which match the pattern, and sections which don't match.
Pattern match, capture only the parsed result with sepCap
Separate the input string into sections which can be parsed as a hexadecimal
number with a prefix "0x"
, and sections which can't. Parse the numbers.
let hexparser = chunk "0x" >> hexadecimal :: Parsec Void String Integer
parseTest (sepCap hexparser) "0xA 000 0xFFFF"
[Right 10,Left " 000 ",Right 65535]
Pattern match, capture only the matched text with findAll
Just get the strings sections which match the hexadecimal parser, throw away
the parsed number.
let hexparser = chunk "0x" >> hexadecimal :: Parsec Void String Integer
parseTest (findAll hexparser) "0xA 000 0xFFFF"
[Right "0xA",Left " 000 ",Right "0xFFFF"]
Pattern match, capture the matched text and the parsed result with findAllCap
Capture the parsed hexadecimal number, as well as the string section which
parses as a hexadecimal number.
let hexparser = chunk "0x" >> hexadecimal :: Parsec Void String Integer
parseTest (findAllCap hexparser) "0xA 000 0xFFFF"
[Right ("0xA",10),Left " 000 ",Right ("0xFFFF",65535)]
Pattern match, capture only the locations of the matched patterns
Find all of the sections of the stream which match
a string of spaces.
Print a list of the offsets of the beginning of every pattern match.
import Data.Either
let spaceoffset = getOffset <* space1 :: Parsec Void String Int
parseTest (return . rights =<< sepCap spaceoffset) " a b "
[0,2,5]
Pattern match balanced parentheses
Find groups of balanced nested parentheses. This is an example of a
“context-free” grammar, a pattern that can't be expressed by a regular
expression. We can express the pattern with a recursive parser.
let parens :: Parsec Void String ()
parens = do
char '('
manyTill
(void (noneOf "()") <|> void parens)
(char ')')
return ()
parseTest (findAll parens) "(()) (()())"
[Right "(())",Left " ",Right "(()())"]
Edit text strings by running parsers with streamEdit
The following examples show how to search for a pattern in a string of text
and then edit the string of text to substitute in some replacement text
for the matched patterns.
Pattern match and replace with a constant
Replace all carriage-return-newline instances with newline.
let crnl = chunk "\r\n" :: Parsec Void String String
streamEdit crnl (const "\n") "1\r\n2\r\n"
"1\n2\n"
Pattern match and edit the matches
Replace alphabetic characters with the next character in the alphabet.
let somelet = some letterChar :: Parsec Void String String
streamEdit somelet (fmap succ) "HAL 9000"
"IBM 9000"
Pattern match and maybe edit the matches, or maybe leave them alone
Find all of the string sections s
which can be parsed as a
hexadecimal number r
,
and if r≤16
, then replace s
with a decimal number. Uses the
match
combinator.
let hexparser = chunk "0x" >> hexadecimal :: Parsec Void String Integer
streamEdit (match hexparser) (\(s,r) -> if r<=16 then show r else s) "0xA 000 0xFFFF"
"10 000 0xFFFF"
Pattern match and edit the matches with IO
Find an environment variable in curly braces and replace it with its
value from the environment.
import System.Environment
let bracevar = char '{' *> manyTill anySingle (char '}') :: ParsecT Void String IO String
streamEditT bracevar getEnv "- {HOME} -"
"- /home/jbrock -"
Context-sensitive pattern match and edit the matches
Capitalize the third letter in a string. The capthird
parser searches for
individual letters, and it needs to remember how many times it has run so
that it can match successfully only on the third time that it finds a letter.
To enable the parser to remember how many times it has run, we'll
compose the parser with a State
monad from
the mtl
package. (Run in ghci
with cabal v2-repl -b mtl
). Because it has
stateful memory, this parser is an example of a “context-sensitive” grammar.
import qualified Control.Monad.State.Strict as MTL
import Control.Monad.State.Strict (get, put, evalState)
import Data.Char (toUpper)
let capthird :: ParsecT Void String (MTL.State Int) String
capthird = do
x <- letterChar
i <- get
put (i+1)
if i==3 then return [x] else empty
flip evalState 1 $ streamEditT capthird (return . fmap toUpper) "a a a a a"
"a a A a a"
In the Shell
If we're going to have a viable sed
replacement then we want to be able
to use it easily from the command line. This
Stack script interpreter
script will find decimal numbers in a stream and replace them with their double.
#!/usr/bin/env stack
{- stack
script
--resolver lts-15
--package megaparsec
--package replace-megaparsec
-}
-- https://docs.haskellstack.org/en/stable/GUIDE/#script-interpreter
import Data.Void
import Text.Megaparsec
import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer
import Replace.Megaparsec
main = interact $ streamEdit (decimal :: Parsec Void String Int) (show . (*2))
If you have
The Haskell Tool Stack
installed then you can just copy-paste this into a file named doubler.hs
and
run it. (On the first run Stack may need to download the dependencies.)
$ chmod u+x doubler.hs
$ echo "1 6 21 107" | ./doubler.hs
2 12 42 214
Alternatives
Some libraries that one might consider instead of this one.
http://hackage.haskell.org/package/regex-applicative
http://hackage.haskell.org/package/pcre-heavy
http://hackage.haskell.org/package/lens-regex-pcre
http://hackage.haskell.org/package/regex
http://hackage.haskell.org/package/pipes-parse
http://hackage.haskell.org/package/stringsearch
http://hackage.haskell.org/package/substring-parser
http://hackage.haskell.org/package/pcre-utils
http://hackage.haskell.org/package/template
Benchmarks
These benchmarks are intended to measure the wall-clock speed
of everything except the actual pattern-matching. Speed of the
pattern-matching is the responsibility of the
megaparsec and
attoparsec
libraries.
The benchmark task is to find all of the one-character patterns x
in a
text stream and replace them by a function which returns the constant
string oo
. So, like the regex s/x/oo/g
.
We have two benchmark input cases, which we call dense and sparse.
The dense case is one megabyte of alternating spaces and x
s
like
x x x x x x x x x x x x x x x x x x x x x x x x x x x x
The sparse case is one megabyte of spaces with a single x
in the middle
like
x
Each benchmark program reads the input from stdin
, replaces x
with oo
,
and writes the result to stdout
. The time elapsed is measured by perf stat
,
and the best observed time is recorded.
See replace-benchmark
for details.
Hypothetically Asked Questions
-
Could we write this library for parsec?
No, because the
match
combinator doesn't exist for parsec. (I can't find it anywhere.
Can it be written?)
-
Is this a good idea?
You may have
heard it suggested
that monadic parsers are better for pattern-matching when
the input stream is mostly signal, and regular expressions are better
when the input stream is mostly noise.
The premise of this library is that monadic parsers are great for finding
small signal patterns in a stream of otherwise noisy text.
There is a reluctance to forego the speedup opportunities afforded by restricting
ourselves to regular grammars, but this is an old superstition about
opportunities which
remain mostly unexploited anyway.
The performance compromise of allowing stack memory allocation (a.k.a. pushdown
automata, a.k.a. context-free grammar) was once considered
controversial for general-purpose programming languages.
I think we
can now resolve that controversy the same way for pattern matching languages.