【问题标题】:Why does try not trigger backtracking in this example为什么在这个例子中尝试不触发回溯
【发布时间】:2020-12-22 06:34:30
【问题描述】:

我正在尝试在 Haskell 中使用 parsec 编写解析器,特别是回溯的工作原理。

采用以下简单的解析器:

import Text.Parsec

type Parser = Parsec String () String

parseConst :: Parser
parseConst =  do {
    x <- many digit;
    return $ read x
}

parseAdd :: Parser
parseAdd = do {
    l <- parseExp;
    char '+';
    r <- parseExp;
    return $ l <> "+" <> r
}

parseExp :: Parser
parseExp = try parseConst <|> parseAdd

pp :: Parser
pp = parseExp <* eof

test = parse pp "" "1+1"

test 有值

Left (line 1, column 2):
unexpected '+'
expecting digit or end of input

在我看来,这应该会成功,因为我在 parseConst 的定义中使用了 try 组合子 parseExp

我错过了什么?我也对如何自己调试这个指针感兴趣,我尝试使用parserTraced,这让我得出结论,它确实没有回溯。

附言。 我知道这是一种编写表达式解析器的糟糕方法,但我想了解它为什么不起作用。

【问题讨论】:

  • try 使parseExprparseConst 失败时尝试使用原始输入parseAdd。但是在这种情况下,parseConst 成功解析了1,所以这个try 什么都不做。

标签: parsing haskell parsec


【解决方案1】:

这里有很多问题。

首先,parseConst 永远无法正常工作。类型说它必须产生一个String,所以read :: String -&gt; String。那个特定的 Read 实例要求输入是一个带引号的字符串,因此如果您尝试评估它产生的值,将 0 个或多个数字字符传递给 read 总是会导致对 error 的调用。

其次,parseConst 可以成功匹配零个字符。我想你可能想要some 而不是many。如果遇到不以数字开头的输入,这将使其实际上失败。

第三,(&lt;|&gt;) 并没有按照你的想法去做。您可能认为(a &lt;* c) &lt;|&gt; (b &lt;* c) 可以与(a &lt;|&gt; b) &lt;* c 互换,但事实并非如此。也没有办法将try 扔进去并使其相同。问题是(&lt;|&gt;) 提交到任何一个成功的分支,如果有的话。在(a &lt;|&gt; b) &lt;* c 中,如果a 匹配,则以后没有办法回溯并在那里尝试b。不管你如何 lob try,它都无法撤销 (&lt;|&gt;) 致力于 a 的事实。相比之下,(a &lt;* c) &lt;|&gt; (b &lt;* c) 直到 acbc 都匹配输入时才会提交。

这是您遇到的情况。经过一些内联后,您有(try parseConst &lt;|&gt; parseAdd) &lt;* eof。因为parseConst 总是会成功(见第二期),parseAdd 永远不会被尝试,即使eof 失败。因此,在parseConst 消耗零个或多个前导数字后,解析将失败,除非这是输入的结尾。解决这个问题本质上需要仔细规划你的语法,这样(&lt;|&gt;) 的任何使用都可以安全地在本地提交。也就是说,每个分支的内容不得以仅由语法后面部分消除歧义的方式重叠。

请注意,(&lt;|&gt;) 的这种令人不快的行为是 parsec 系列库的工作方式,而不是 Haskell 中所有解析器库的工作方式。其他库在没有 parsec 系列选择的左偏或提交行为的情况下工作。

【讨论】:

  • 谢谢!在我达到以前的 YACC 水平之前,我似乎还有很多工作要做:)。
猜你喜欢
  • 1970-01-01
  • 2017-08-17
  • 2021-04-28
  • 2018-06-15
  • 2021-11-12
  • 1970-01-01
  • 1970-01-01
  • 2012-12-23
  • 1970-01-01
相关资源
最近更新 更多