使用MonadError和Parsec

3

我正在尝试将MonadError与Parsec一起使用。我想出了以下代码片段:

f5 = do
    char 'a'
    throwError "SomeError"

f6 = f5 `catchError` (\e -> unexpected $ "Got the error: " ++ e)

ret = runErrorT (runParserT f6 () "stdin" "a")

然而,retLeft "SomeError",看起来 catchError 没有产生任何作用。在这里使用 MonadError 的正确方式是什么?

我更愿意使用 MonadError 而不是 Parsec 自己的错误处理,例如当我有:

try (many1 parser1) <|> parser2

如果解析器1在这里失败,解析器2将继续工作,但我希望有一个异常来完全中止解析。
3个回答

5

我认为你试图错误地使用MonadError

try (many1 parser1) <|> parser2中,你想避免的行为源于使用了try<|>——如果你不喜欢它,可以使用不同的组合子。也许像(many1 parser1) >> parser2这样的表达式更适合你?(这会丢弃(many1 parser1)的结果;当然你可以使用>>=(many1 parser1)的结果与parser2的结果结合起来。)


(注意:在此之下,没有真正好的解决方案,只是一些对为什么某些事情可能行不通的思考...希望这可能会有所启发,但不要期望太多。)

对ParsecT / MonadError交互的更详细的研究。恐怕它有点混乱,我仍然不确定如何最好地做到OP想做的事情,但我希望以下内容至少能够提供有关原始方法缺乏成功的原因的见解。

首先,需要注意的是不能说Parsec是MonadError的实例。当内部单子为Identity时,Parsec是由ParsecT产生的单子;只有当ParsecT被赋予一个内部单子,并且该单子本身是MonadError的实例时,ParsecT才会产生MonadError的实例。以下是GHCi交互的相关片段:

> :i Parsec
type Parsec s u = ParsecT s u Identity
    -- Defined in Text.Parsec.Prim
-- no MonadError instance

instance (MonadError e m) => MonadError e (ParsecT s u m)
  -- Defined in Text.Parsec.Prim
-- this explains why the above is the case
-- (a ParsecT-created monad will only become an instance of MonadError through
-- this instance, unless of course the user provides a custom declaration)

接下来,让我们通过使用catchError和ParsecT来进行一个工作示例。考虑以下GHCi交互:

> (runParserT (char 'a' >> throwError "some error") () "asdf" "a" :: Either String (Either ParseError Char)) `catchError` (\e -> Right . Right $ 'z')
Right (Right 'z')

类型注释似乎是必需的(这对我来说很有直觉意义,但它与原始问题无关,所以我不会试图阐述)。 GHC 确定整个表达式的类型如下:

Either String (Either ParseError Char)

因此,我们得到了一个常规的解析结果——Either ParseError Char——用Either String单子替换了通常的Identity单子。由于Either StringMonadError的一个实例,我们可以使用throwError/catchError,但是传递给catchError的处理程序必须产生正确类型的值。对于中断解析过程来说,这并不是非常有用。 回到问题中的示例代码。它做的事情略有不同。让我们检查在问题中定义的ret的类型:
forall (m :: * -> *) a.
(Monad m) =>
m (Either [Char] (Either ParseError a))

根据GHCi的提示...请注意,我必须使用{-# LANGUAGE NoMonomorphismRestriction #-}来解除单态限制,以便代码在没有类型注释的情况下编译。

该类型是对使用ret进行有趣操作的可能性的提示。我们开始吧:

> runParserT ret () "asdf" "a"
Right (Left "some error")

回过头来看,传递给 catchError 的处理程序使用 unexpected 生成了一个值,所以当然它将成为(可用作)解析器...但我担心我不知道如何将其转化为有用的内容,以便打破解析过程。


我想在任意点中止解析过程,“try”只是一个例子。 - Johannes Bittner
为了快速而不精确地进行调试,您可以在链中插入类似于error "message"的内容...比如说,try (many1 parser1) <|> error "kaboom!" <|> parser2。然而,这会导致程序崩溃,因此如果您想在记录错误条件的同时处理一堆字符串,这种方法是不可接受的...我会看看是否有更好的解决方案。 - Michał Marczyk
嗯,我不确定这是否会对你有所帮助,但我已经尝试分析了发生了什么,并将结果编辑到答案中...如果这有用,那太好了,如果没有,请告诉我,我可能会删除整个内容。然而,需要意识到的一件事是,Parsec实际上并不是MonadError的一个实例(与使用ParsecT构造的其他某些monad相反),这一点非常重要。 - Michał Marczyk

2
如果您正在尝试调试解析器以进行故障排除,那么最简单的方法可能是使用errorDebug.Trace等。另一方面,如果您需要在实际程序中终止某些输入的解析,但由于try (...) <|>构造而无法这样做,则说明您的逻辑存在错误,应该停下来重新考虑语法,而不是通过错误处理进行黑客攻击。如果您希望解析器有时会在给定输入上终止,但在其他情况下不会,则可能缺少输入流(应该添加)或解析器不是解决问题的方法。如果您希望解析器从非致命错误中恢复正常,并在可能时继续尝试,但在无法继续时终止并显示错误,则...也许应该考虑使用不同于Parsec的东西,因为它真的不是为此设计的。我相信乌得勒支大学的Haskell解析器组合库更容易支持这种逻辑。至于Parsec本身是否是MonadError的一个实例--是的,它自己的错误处理包含了这个功能。您要做的是在Parsec之上堆叠第二个错误monad,并且您可能会遇到麻烦,因为通常很难区分这种方式中“冗余”的monad transformer。处理多个State monad更为麻烦,这就是为什么Parsec(也是一个State monad)提供了保持自定义状态的功能。换句话说,Parsec作为错误monad并没有帮助您,实际上只会使问题更加困难。

为什么它没有设计成这样?我的意思是,Parsec是MonadError的一个实例。我不想像你提到的那样使用hack,只是有些情况下,我想通过异常来中止解析过程。说实话,我更感兴趣的是为什么我的代码不起作用,而不是为什么我根本不应该使用MonadError。 - Johannes Bittner
1
它并不是为了通用错误恢复而设计的,因为在解析中这是一个非常困难的问题;Parsec通常基于一种假设来工作:解析器要么成功,要么无害且立即失败,要么由于消耗输入而无法恢复。Parsec自己的错误处理对于所有这些情况应该足够了;如果你需要超越这个范围,那么很可能Parsec并不适合你的任务。 - C. A. McCann

2
如果您需要在实际程序中终止某些输入的解析,但由于try (...) <|>结构而未能这样做,则您的逻辑存在错误,应停止并重新考虑语法,而不是通过错误处理来解决它。
如果您希望解析器有时能够终止给定的输入,但有时不能,那么可能是输入流中缺少了某些内容(需要添加),或者解析器不是您的问题的解决方案。
这个答案基于一个假设,即问题出在语法上。但是,如果我使用语法来提供编译器,那么还有其他语法无法处理的错误。比如说变量引用一个未定义的变量。并且该语言被指定为单次遍历,变量在遇到时进行评估。那么,语法就很好,解析也很好。但由于评估语法中指定的内容时发生了错误,现有的“失败”或“意外”或不足以处理此问题。最好有一种方法可以中止解析,而不必诉诸于更高级别的错误处理。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接