Parsec:处理重叠的解析器

Parsec: Handling Overlapping Parsers

我对 Haskell 中的解析真的很陌生,但它基本上是有道理的。

我正在构建一个模板程序,主要是为了更好地学习解析;模板可以通过 {{ value }} 表示法插入值。

这是我当前的解析器,

data Template a = Template [Either String a]
data Directive = Directive String

templateFromFile :: FilePath -> IO (Either ParseError (Template Directive))
templateFromFile = parseFromFile templateParser

templateParser :: Parser (Template Directive)
templateParser = do
  tmp <- template
  eof
  return tmp

template :: Parser (Template Directive)
template = Template <$> many (dir <|> txt)
    where
      dir = Right <$> directive
      txt = Left <$> many1 (anyChar <* notFollowedBy directive)

directive :: Parser Directive
directive = do
  _ <- string "{{"
  txt <- manyTill anyChar (string "}}")
  return $ Directive txt

然后我 运行 它在一个像这样的文件中:

{{ value }}

This is normal Text

{{ expression }}

当我 运行 使用 templateFromFile "./template.txt" 时,我得到错误:

Left "./template.txt" (line 5, column 17):
unexpected Directive " expression "

为什么会发生这种情况,我该如何解决?

我的基本理解是many1 (anyChar <* notFollowedBy directive) 应该抓住所有字符直到下一个指令开始,然后应该失败并且 return 字符列表直到那一点;然后 它应该回落到之前的 many 并且应该再次尝试解析 dir 并且应该会成功;显然还有其他事情正在发生。我是 无法弄清楚如何解析事物 between other things when 解析器大部分重叠。

我想要一些关于如何更地道地构建它的提示,如果我以愚蠢的方式做某事,请告诉我。干杯!感谢您的宝贵时间!

many1 (anyChar <* notFollowedBy directive)

这只解析后面没有指令的字符。

{{ value }}

This is normal Text

{{ expression }}

当解析中间的文本时,它会在最后一个 t 处停止,留下未使用指令之前的换行符(因为它是一个字符后跟一个指令),所以下一次迭代,您尝试解析指令但失败了。然后你在那个换行符上重试 txt,解析器希望它后面没有指令,但它找到了一个,因此错误。

你有几个问题。首先,在 Parsec 中,如果解析器使用任何输入然后失败,那就是一个错误。所以,当解析器:

anyChar <* notFollowedBy directive

失败(因为字符后跟一个指令),它失败after anyChar已经消耗了输入,并且立即产生错误。因此,解析器:

let p1 = many1 (anyChar <* notFollowedBy directive)

如果遇到指令,将永远不会成功。例如:

parse p1 "" "okay"   -- works
parse p1 "" "oops {{}}"  -- will fail after consuming "oops "

您可以通过插入 try 子句来解决此问题:

let p2 = many1 (try (anyChar <* notFollowedBy directive))
parse p2 "" "okay {{}}"

产生 Right "okay" 并揭示了第二个问题。解析器 p2 仅使用指令后面没有的字符,因此排除了指令之前的 space ,并且您无法在解析器中使用 的字符是后面跟着一个指令,所以卡住了。

你实际上想要这样的东西:

let p3 = many1 (notFollowedBy directive *> anyChar)

首先检查在当前位置,我们在抓取字符之前没有查看指令。不需要 try 子句,因为如果失败,它会在不消耗输入的情况下失败。 (根据文档,notFollowedBy 从不消耗输入。)

parse p3 "" "okay" -- returns Right "okay"
parse p3 "" "okay {{}}" -- return Right "okay "
parse p3 "" "{{fails}}"  -- correctly fails w/o consuming input

因此,以您的原始示例为例:

txt = Left <$> many1 (notFollowedBy directive *> anyChar)

应该可以正常工作。

replace-megaparsec 是一个用于使用解析器进行搜索和替换的库。这 搜索和替换功能是 streamEdit, 它可以找到您的 {{ value }} 模式,然后替换为其他文本。

streamEdit 是从通用版本构建的 您的 template 函数调用 sepCap.

import Replace.Megaparsec
import Text.Megaparsec
import Text.Megaparsec.Char
import Data.Char

input = unlines
    [ "{{ value }}"
    , ""
    , "This is normal Text"
    , ""
    , "{{ expression }}"
    ]

directive :: Parsec Void String String
directive = do
    _ <- string "{{"
    txt <- manyTill anySingle (string "}}")
    return  txt

editor k = fmap toUpper k

streamEdit directive editor input
 VALUE 

This is normal Text

 EXPRESSION