当 Parsec 解析器(如 parse_atom)在特定字符串上运行时,有四种可能的结果:
- 它成功了,消耗了一些输入。
- 失败,消耗了一些输入。
- 成功,不消耗任何输入。
- 失败,不消耗任何输入。
在 Parsec 源代码中,它们被称为“consumed ok”、“consumed err”、“empty ok”和“empty err”(有时缩写为 cok、cerr、eok、eerr)。
当两个 Parsec 解析器用于替代方案时,例如 p <|> q,下面是它的解析方式。首先,Parsec 尝试使用p 进行解析。那么:
- 如果这导致“consumed ok”或“empty ok”,则解析成功,这将成为整个解析器
p <|> q的结果。
- 如果这导致“空错误”,Parsec 会尝试替代
q,这将成为整个 p <|> q 解析器的结果。
- 如果这导致“consumed err”,则整个解析器
p <|> q 将失败并显示“consumed err” (cerr)。
注意p 返回 cerr(导致整个解析器失败)与返回 eerr(导致替代解析器 q 被尝试)之间的关键区别。
try 函数通过将“cerr”结果转换为“eerr”结果来改变解析器的行为。
这意味着如果您尝试使用不同的解析器解析文本"s(t)":
- 使用解析器
parse_atom <|> parse_op,解析器 parse_atom 返回“cok”,使用 "s" 并留下无法解析的文本 "(t)",这会导致错误
- 使用解析器
try parse_atom <|> parse_op,解析器parse_atom 仍然返回消耗"s"的“cok”,因此try(仅将cerr更改为eerr)无效,并且无法解析的文本 "(t)" 导致相同的错误
- 使用解析器
parse_op <|> parse_atom,解析器parse_op成功解析字符串(其实不是因为递归调用parse_exp不能解析"t",但我们忽略它);但是,如果在文本 "s" 上使用相同的解析器,那么 parse_op 将在失败(即 cerr)之前消耗 "s",导致整个解析失败而不是尝试替代 parse_atom
- 使用解析器
try parse_op <|> parse_atom,这将解析"s(t)",与前面的示例完全相同,而try 将不起作用;但是,它也对文本 "s" 起作用,因为 parse_op 会在 cerr 失败之前消耗 "s",然后 try 会通过将 cerr 转换为一个错误,将检查替代parse_atom,成功解析(cok)原子"s"。
这就是为什么你的问题的“正确”解析器是try parse_op <|> parse_atom。
请注意,这种行为不是一元解析器的基本方面。这是 Parsec(以及兼容的解析器,如 Megaparsec)做出的设计选择。其他 monadic 解析器对于 <|> 的替代方法的工作方式可能有不同的规则。
这类 Parsec 解析问题的“一般解决方法”是注意表达式p <|> q 中的事实:
-
p 首先尝试,如果成功,q 将被忽略,即使 q 会提供“更长”或“更好”或“更明智”的解析或避免进一步的解析错误。在parse_atom <|> parse_op 中,因为parse_atom 可以在用于parse_op 的字符串上成功,所以此顺序将无法正常工作。
-
q 仅在 p 失败不消耗输入时才尝试。您必须安排p 在失败时不消耗任何东西,可能通过使用try,如果您希望检查替代q。因此,如果 parse_op 在意识到它无法继续并返回 cerr 之前开始使用某些东西(如标识符),parse_op <|> 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 <|> parse_atom 这样的“优雅”。 (不过,它的性能更好,因此在某些应用程序中可能需要考虑这一点。)