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