1

在阅读 Haskell 的书时,我遇到了trifecta

我试图绕开我的头,但仍然无法理解 <|>

我有以下问题。

简单来说 (<|>) = Monadic 选择?

p = a <|> b -- 使用解析器a如果没有则使用b

如果是,那么为什么下面的解析器失败了?

parseFraction :: Parser Rational
parseFraction = do
    numerator <- decimal
    char '/'
    denominator <- decimal
    case denominator of 
        0 -> fail "denominator cannot be zero"
        _ -> return (numerator % denominator)


type RationalOrDecimal = Either Rational Integer
parseRationalOrDecimal = (Left <$> parseFraction) <|> (Right<$> decimal)


main = do
    let p f i = parseString f mempty i
    print $ p (some (skipMany (oneOf "\n")  *> parseRationalOrDecimal <* skipMany (oneOf "\n"))) "10"

在完美世界中,如果 a 是parseFraction将失败,那么<|>应该使用小数,但事实并非如此。但是当我使用尝试它的工作原理。

  1. 我错过了什么?
  2. 为什么我们需要在 <|> 应该在第一次失败时运行第二个解析器时使用try ?

parseRationalOrDecimal = try (Left <$> parseFraction) <|> (Right<$> decimal)

4

1 回答 1

1

原因是因为parseFraction在失败之前消耗了输入,因此,它被认为是选择中的正确分支。让我给你和例子:

假设你正在编写一个 python 解析器,你必须决定一个声明是一个class还是一个函数(关键字def),然后你写

parseExpresion = word "def" <|> word "class" -- DISCLAIMER: using a ficticious library

然后如果用户写入def或者class它会匹配,但是如果用户写入det它将尝试第一个分支并匹配,然后因为找到de而无法匹配预期。它不会费心尝试下一个解析器,因为错误被认为是在第一个分支中。尝试解析器没有什么意义,因为错误可能在第一个分支中。ftclass

在您的情况下parseFraction,匹配一些数字然后因为/找不到而失败,然后它就不会费心尝试decimal解析器。

这是一个设计决定,其他一些库使用不同的约定(例如:Attoparsec总是在失败时回溯),并且一些函数声称“不消耗输入”(例如notFollowedBy:)

请注意,这里有一个权衡:

第一:如果<|>行为符合您的预期,则如下

parse parseRationalOrDecimal "123456789A"

将首先解析所有数字,直到找到“A”,然后再次解析!直到找到“A”之前的所有数字......所以两次执行相同的计算只是为了返回失败。

第二:如果您更关心错误消息,则当前行为更方便。按照 python 示例,想象一下:

parseExpresion = word "def" <|> word "class" <|> word "import" <|> word "type" <|> word "from"

如果用户键入“frmo”,解析器将转到最后一个分支,并会引发类似错误,expected "from" but "frmo" was found而如果必须检查所有替代项,则错误将更像是expected one of "def", "class", "import", "type" of "from"与实际拼写错误不太接近的错误。

正如我所说,这是一个图书馆设计决定,我只是想说服你有充分的理由不自动尝试所有替代方案,并try在你明确想要这样做时使用。

于 2021-09-08T09:52:59.850 回答