【问题标题】:Problem while writing a small parser in Haskell using Parsec使用 Parsec 在 Haskell 中编写小型解析器时出现问题
【发布时间】:2020-10-17 03:48:01
【问题描述】:

我正在尝试使用以下代码为一种小型语言编写解析器

import Text.ParserCombinators.Parsec
import Text.Parsec.Token

data Exp = Atom String | Op String Exp

instance Show Exp where
  show (Atom x) = x
  show (Op f x) = f ++ "(" ++ (show x) ++ ")"

parse_exp :: Parser Exp
parse_exp = (try parse_atom) <|> parse_op

parse_atom :: Parser Exp
parse_atom = do
  x <- many1 letter
  return (Atom x)

parse_op :: Parser Exp
parse_op = do
  x <- many1 letter
  char '(' 
  y <- parse_exp
  char ')'
  return (Op x y)

但是当我输入 ghci 时

>>> parse (parse_exp <* eof) "<error>" "s(t)"

我得到了输出

Left "<error>" (line 1, column 2):
unexpected '('
expecting letter or end of input

如果我将parse_exp 重新定义为

parse_exp = (try parse_op) <|> parse_atom

然后我得到正确的结果

>>> parse (parse_exp <* eof) "<error>" "s(t)"
Right s(t)

但我很困惑为什么第一个不起作用。解析中的这类问题是否有通用的解决方法?

【问题讨论】:

    标签: parsing haskell parsec


    【解决方案1】:

    当 Parsec 解析器(如 parse_atom)在特定字符串上运行时,有四种可能的结果:

    1. 它成功了,消耗了一些输入。
    2. 失败,消耗了一些输入。
    3. 成功,不消耗任何输入。
    4. 失败,不消耗任何输入。

    在 Parsec 源代码中,它们被称为“consumed ok”、“consumed err”、“empty ok”和“empty err”(有时缩写为 cok、cerr、eok、eerr)。

    当两个 Parsec 解析器用于替代方案时,例如 p &lt;|&gt; q,下面是它的解析方式。首先,Parsec 尝试使用p 进行解析。那么:

    • 如果这导致“consumed ok”或“empty ok”,则解析成功,这将成为整个解析器p &lt;|&gt; q的结果。
    • 如果这导致“空错误”,Parsec 会尝试替代 q,这将成为整个 p &lt;|&gt; q 解析器的结果。
    • 如果这导致“consumed err”,则整个解析器 p &lt;|&gt; q 将失败并显示“consumed err” (cerr)。

    注意p 返回 cerr(导致整个解析器失败)与返回 eerr(导致替代解析器 q 被尝试)之间的关键区别。

    try 函数通过将“cerr”结果转换为“eerr”结果来改变解析器的行为。

    这意味着如果您尝试使用不同的解析器解析文本"s(t)"

    • 使用解析器 parse_atom &lt;|&gt; parse_op,解析器 parse_atom 返回“cok”,使用 "s" 并留下无法解析的文本 "(t)",这会导致错误
    • 使用解析器try parse_atom &lt;|&gt; parse_op,解析器parse_atom 仍然返回消耗"s"的“cok”,因此try(仅将cerr更改为eerr)无效,并且无法解析的文本 "(t)" 导致相同的错误
    • 使用解析器parse_op &lt;|&gt; parse_atom,解析器parse_op成功解析字符串(其实不是因为递归调用parse_exp不能解析"t",但我们忽略它);但是,如果在文本 "s" 上使用相同的解析器,那么 parse_op 将在失败(即 cerr)之前消耗 "s",导致整个解析失败而不是尝试替代 parse_atom
    • 使用解析器try parse_op &lt;|&gt; parse_atom,这将解析"s(t)",与前面的示例完全相同,而try 将不起作用;但是,它对文本 "s" 起作用,因为 parse_op 会在 cerr 失败之前消耗 "s",然后 try 会通过将 cerr 转换为一个错误,将检查替代parse_atom,成功解析(cok)原子"s"

    这就是为什么你的问题的“正确”解析器是try parse_op &lt;|&gt; parse_atom

    请注意,这种行为不是一元解析器的基本方面。这是 Parsec(以及兼容的解析器,如 Megaparsec)做出的设计选择。其他 monadic 解析器对于 &lt;|&gt; 的替代方法的工作方式可能有不同的规则。

    这类 Parsec 解析问题的“一般解决方法”是注意表达式p &lt;|&gt; q 中的事实:

    • p 首先尝试,如果成功,q 将被忽略,即使 q 会提供“更长”或“更好”或“更明智”的解析或避免进一步的解析错误。在parse_atom &lt;|&gt; parse_op 中,因为parse_atom 可以在用于parse_op 的字符串上成功,所以此顺序将无法正常工作。
    • q 仅在 p 失败不消耗输入时才尝试。您必须安排p 在失败时不消耗任何东西,可能通过使用try,如果您希望检查替代q。因此,如果 parse_op 在意识到它无法继续并返回 cerr 之前开始使用某些东西(如标识符),parse_op &lt;|&gt; parse_atom 将无法工作。

    作为使用try 的替代方法,您还可以更仔细地考虑解析器的结构。例如,写parse_exp 的另一种方式是:

    parse_exp :: Parser Exp
    parse_exp = do
      -- there's always an identifier
      x <- many1 letter
      -- there *might* be an expression in parentheses
      y <- optionMaybe (parens parse_exp)
      case y of
        Nothing -> return (Atom x)
        Just y' -> return (Op x y')
    
      where parens = between (char '(') (char ')')
    

    这可以写得更简洁一些,但即便如此,它也不像try parse_op &lt;|&gt; parse_atom 这样的“优雅”。 (不过,它的性能更好,因此在某些应用程序中可能需要考虑这一点。)

    【讨论】:

      【解决方案2】:

      问题是字符串"s" 根据您的定义算作一个原子。试试这个:

      parse parse_atom "" "s(t)"
      > Atom "s"
      

      所以你的解析器 parse_exp 实际上成功了,返回了Atom "s",但是你也期望它后面有一个 EOF,这就是它失败的地方,遇到一个开放的括号而不是 EOF(就像错误消息所说的那样! )

      当您交换替代方案时,它将首先尝试parse_op,这会成功,返回Op "s" "t",然后遇到EOF,正如预期的那样。

      【讨论】:

      • 有没有办法可以正确解析整个字符串?如果我删除 eof 则解析器在 s 处停止,但我想解析整个字符串“s(t)”
      • 这就是翻转替代品的作用
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多