【问题标题】:Why does attoparsec need manyTill if it backtracks?如果 attoparsec 回溯,为什么需要 manyTill?
【发布时间】:2020-06-26 00:08:57
【问题描述】:

考虑这些不同的解析器组合器的用法。

import Control.Applicative.Combinators
import Text.Regex.Applicative
 
main :: IO ()
main = do
  let parser1 = sym '"' *> manyTill anySym (sym '"')
  print $ match parser1 "\"abc\""
  let parser2 = sym '"' *> many anySym <* sym '"'
  print $ match parser2 "\"abc\""
import Control.Applicative.Combinators            
import Text.ParserCombinators.ReadP hiding(many, manyTill)
 
main :: IO ()
main = do
  let parser1 = char '"' *> manyTill get (char '"')
  print $ readP_to_S parser1 "\"abc\""
  let parser2 = char '"' *> many get <* char '"'
  print $ readP_to_S parser2 "\"abc\""
{-# LANGUAGE OverloadedStrings #-}
 
import Control.Applicative.Combinators
import Data.Attoparsec.Text hiding(manyTill)
 
main :: IO ()
main = do
  let parser1 = char '"' *> manyTill anyChar (char '"')
  print $ parseOnly parser1 "\"abc\""
  let parser2 = char '"' *> many anyChar <* char '"'
  print $ parseOnly parser2 "\"abc\""
import Control.Applicative.Combinators
import Text.Megaparsec hiding(many, manyTill)
import Data.Void

main :: IO ()
main = do
  let parser1 = single '"' *> manyTill anySingle (single '"') :: Parsec Void String String
  print $ parseMaybe parser1 "\"abc\""
  let parser2 = single '"' *> many anySingle <* single '"' :: Parsec Void String String
  print $ parseMaybe parser2 "\"abc\""

对于这四个,manyTill 解析器成功匹配abc,因为这不依赖于回溯。使用regex-applicativeReadPmany 解析器也成功匹配abc,因为它们都默认回溯。使用megaparsecmany 解析器无法匹配,因为默认情况下它不会回溯。到目前为止,一切都说得通。然而,使用attoparsecmany 解析器无法匹配,即使它确实回溯:its documentation 说“attoparsec 解析器总是在失败时回溯”和“如果您将增量输入提供给解析器,它将需要内存与您提供的输入量成正比。(这是支持任意回溯所必需的。)“。为什么是这样?回溯不应该就是这样吗?

【问题讨论】:

    标签: parsing haskell backtracking parser-combinators attoparsec


    【解决方案1】:

    Attoparsec 文档中“回溯”的含义与其他回溯解析器的回溯含义不同。

    在将try 用于 Parsec 或 Megaparsec 解析器时,有助于查看“回溯”的含义。这些解析器具有消耗输入后失败(“consume err” = cerr)与不消耗任何内容后失败(“empty err” = err)的概念。对于这些解析器,p &lt;|&gt; q 替代运算符处理 p 的失败,如果它是 cerr(立即使整个 p &lt;|&gt; q 失败)与 eerr(尝试替代 q)。 try 函数通过将 cerr 转换为 eerr 来回溯。也就是说,try p &lt;|&gt; q 将在p 因 cerr 失败的情况下“回溯”输入流的错误消耗。这是在备选方案中对失败进行单步回溯(尽管使用嵌套的try 调用,可以在解析失败的序列/级联中执行多个回溯步骤)。

    Attoparsec 不区分 cerr 和 eerr,所以就好像所有解析器都被 try 调用包围。这意味着它会自动执行在备选方案中失败时回溯的多个步骤

    ReadP 隐式回溯,同时并行评估每个可能的解析,丢弃那些曾经失败的解析,然后选择剩下的“第一个”解析。 它在所有可能解析的树上“回溯”故障,无论故障是否在替代上下文中生成。

    事实证明,“在替代方案中对失败进行多步回溯”是一种比“在所有可能的解析树上回溯”更温和的回溯形式。

    几个简化的示例可能有助于显示差异。考虑解析器:

    (anyChar *> char 'a') <|> char 'b'
    

    和输入字符串"bd"。此解析器因 Parsec/Megaparsec 而失败。左侧替代方案在失败之前使用"b"anyChar,消耗了输入(cerr),整个解析器失败。但是,这与 Attoparsec 一起工作得很好:左侧替代方案在 char 'a' 处失败,并且 Attoparsec 在尝试成功的char 'b' 替代方案中回溯此失败。它也适用于 ReadP,它并行构造所有可能的解析,然后在 char 'a' 失败时从左侧替代方案中丢弃解析,从而导致 char 'b' 进行一次成功解析。

    现在,考虑解析器:

    (anyChar <|> pure '*') *> char 'b'
    

    和输入字符串"b"。 (回想一下,pure '*' 不消耗任何内容并且总是成功。)此解析器因 Parsec/Megaparsec 而失败,因为anyChar 解析"b"pure '*' 被忽略,并且空字符串与char 'b' 不匹配. Attoparsec 也失败了:anyChar 成功解析了"b",并且在替代方案的上下文中没有失败,因此没有回溯尝试pure '*' 替代方案。使用char 'b' 解析空字符串的尝试随后失败。 (这个失败,如果它发生在另一个替代方案的上下文中,可能会导致回溯那个替代方案,但永远不会重新考虑这个pure '*'替代方案。)

    相比之下,ReadP 可以很好地解析。 ReadP 并行解析备选方案,考虑到 anyChar 解析 "b"pure '*' 什么都不解析。尝试char 'b' 解析时,前者失败,后者成功。

    回到你的例子。用 Attoparsec 解析时,因为:

    many p = ((:) <$> p <*> many p) <|> pure []
    

    左边的选项(:) &lt;$&gt; anyChar &lt;*&gt; many anyChar 将继续成功匹配,直到anyChar 匹配右引号。在 EOF 时,左侧将失败(不消耗输入,尽管 Attoparsec 不关心这一点),右侧将成功。替代方案中唯一的失败是在 EOF,它没有消耗任何东西,因此 Attoparsec 的自动“回溯”不起作用; Megaparsec 会做同样的事情。不管怎样,一旦这个many anyChar成功了,就不会再访问它了,即使终止的char '"'随后失败了。

    所以,就是为什么您需要manyTill 来显式监视终止字符。

    【讨论】:

    • 好的,我认为这是有道理的。这是一个有效的总结吗? "attoparsec 只有在&lt;|&gt; 的左侧消耗了一些字符然后失败时才会回溯。如果&lt;|&gt; 的左侧成功,与ReadPregex-applicative 不同,attoparsec 永远不会尝试右侧,即使稍后解析失败。”
    • 您如何解释Megaparsec 在 OP 示例中的行为?
    • @dfeuer -- 它也失败了,对吧?在 OP 的示例中,替代方案中的唯一失败是不消耗任何输入的失败(当 anyChar 在 EOF 上失败时),因此 Megaparsec 和 Attoparsec 的行为相同。
    • @JosephSible-ReinstateMonica,是的,我认为这个总结是正确的。
    猜你喜欢
    • 1970-01-01
    • 2013-01-29
    • 2012-04-03
    • 2014-08-24
    • 1970-01-01
    • 2022-01-07
    • 2018-07-12
    • 1970-01-01
    • 2017-08-24
    相关资源
    最近更新 更多