Parsing 使用Parsec在Haskell中编写小型解析器时出现问题
我正试图用下面的代码为一种小型语言编写一个解析器Parsing 使用Parsec在Haskell中编写小型解析器时出现问题,parsing,haskell,parsec,Parsing,Haskell,Parsec,我正试图用下面的代码为一种小型语言编写一个解析器 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 :
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)
然后我得到正确的结果
>>> parse (parse_exp <* eof) "<error>" "s(t)"
Right s(t)
>>parse(parse_exp问题是字符串“s”
根据您的定义计算为一个原子。请尝试以下操作:
parse parse_atom "" "s(t)"
> Atom "s"
因此,您的解析器parse_exp
实际上成功了,返回了Atom“s”
,但是您也期望在它之后出现一个EOF,这就是它失败的地方,遇到的是一个打开的paren而不是EOF(就像错误消息所说的!)
当您交换备选方案时,它将首先尝试parse_op
,这将成功,返回op“s”t
,然后遇到EOF,正如预期的那样。当Parsec解析器(如parse_atom
)在特定字符串上运行时,有四种可能的结果:
它成功了,消耗了一些输入
它失败了,消耗了一些输入
它成功了,不消耗任何输入
它失败,不消耗任何输入
在Parsec源代码中,这些被称为“消耗的ok”、“消耗的err”、“空的ok”和“空的err”(有时缩写为cok、cerr、eok、eer)
当在另一种情况下使用两个Parsec解析器时,如pq
,以下是解析方式。首先,Parsec尝试使用p
进行解析。然后:
- 如果这导致“已消费ok”或“空ok”,则解析成功,这将成为整个解析器的结果
pq
- 如果这导致“empty err”,Parsec将尝试替代
q
,这将成为整个pq
解析器的结果
- 如果这导致“消耗的错误”,则整个解析器
pq
将以“消耗的错误”(cerr)失败
注意p
返回cerr(导致整个解析器失败)与返回eer(导致尝试替代解析器q
)之间的关键区别
try
函数通过将“cerr”结果转换为“eerr”结果来更改解析器的行为
这意味着,如果您试图使用不同的解析器解析文本“s(t)”
:
- 使用解析器
parse_atom parse_op
,解析器parse_atom
返回“cok”,消耗“s”
,并留下不可解析的文本“(t)”
,从而导致错误
- 使用解析器
try-parse\u-atom-parse\u-op
,解析器parse\u-atom
仍然返回“cok”消费“s”
,因此try
(仅将cerr更改为eerr)无效,并且不可解析的文本(t)
导致相同的错误
- 使用解析器
parse\u op parse\u atom
,解析器parse\u op
成功解析字符串(实际上,这不是因为对parse\u exp
的递归调用无法解析“t”
,但让我们忽略这一点);但是,如果对文本“s”使用相同的解析器
,则parse\u op
将在失败之前消耗“s”
(即cerr),导致整个解析失败,而不是尝试替代的parse\u atom
- 使用解析器
try parse_op parse_atom
,这将解析“s(t)”
,与前面的示例完全相同,try
将没有任何效果;但是,它也将对文本“s”
,因为parse_op
将消耗“s”
在cerr失败之前,然后try
将通过将cerr转换为eerr来“拯救”解析,并检查备选parse_atom
,成功解析(cok)atom“s”
这就是为什么针对您的问题的“正确”解析器是尝试parse\u op parse\u atom
请注意,这种行为不是一元语法分析器的一个基本方面。它是由Parsec(以及兼容的语法分析器,如Megaparsec)做出的设计选择。其他一元语法分析器可以有不同的规则来说明
的替代方案如何工作
这类Parsec解析问题的“一般解决方案”是了解表达式pq
中的事实:
p
首先尝试,如果成功,q
将被忽略,即使q
将提供“更长的”或“更好的”或“更合理的”解析或避免进一步出现其他解析错误。在parse_atom parse_op
中,由于parse_atom
可以在用于parse_op
的字符串上成功,因此此顺序将无法正常工作
q
只有在p
失败而不消耗输入时才会尝试。如果您希望检查备选方案q
,则必须安排p
在失败时不消耗任何东西,可能通过使用try
。因此,如果parse\u op,则parse\u-opparse\u-atom
不起作用e> 在意识到无法继续并返回cerr之前,开始使用某些东西(如标识符)
除了使用try
,您还可以更仔细地考虑解析器的结构。例如,编写parse\u 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 ')')
parse_exp::Parser exp
parse_exp=do
--总有一个标识符
有没有方法可以正确解析整个字符串?如果我删除eof,那么解析器将停止在s,但我想解析整个字符串“s(t)”,这就是翻转的AtAlternative将要做的
>>> parse (parse_exp <* eof) "<error>" "s(t)"
Right s(t)
parse parse_atom "" "s(t)"
> Atom "s"
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 ')')