【问题标题】:Parsec: backtracking not workingParsec:回溯不起作用
【发布时间】:2010-03-08 18:57:36
【问题描述】:

我正在尝试解析 F# 类型的语法。我开始编写 [F] Parsec 语法并遇到问题,因此我将the grammar 简化为:

type ::= identifier | type -> type
identifier ::= [A-Za-z0-9.`]+

在遇到 FParsec 问题后,我切换到 Parsec,因为我有一个 full chapter of a book dedicated to explaining it。我的这个语法代码是

typeP = choice [identP, arrowP]
identP = do
   id <- many1 (digit <|> letter <|> char '.' <|> char '`')
   -- more complicated code here later
   return id
arrowP = do
  domain <- typeP
  string "->"
  range <- typeP
  return $ "("++domain++" -> "++range++")"
run = parse (do t <- typeP
                eof
                return t) "F# type syntax"

问题是 Parsec 默认不回溯,所以

> run "int"
Right "int"
-- works! 
> run "int->int"
Left "F# type syntax"
unexpected "-"
expecting digit, letter, ".", "`" or end of input
-- doesn't work!

我尝试的第一件事是重新排序 typeP:

typeP = choice [arrowP, identP]

但这只是堆栈溢出,因为语法是左递归的——typeP 永远不会尝试identP,因为它会一遍又一遍地尝试arrowP。接下来我在各个地方尝试了try,例如:

typeP = choice [try identP, arrowP]

但我所做的一切似乎都没有改变 (1) 堆栈溢出或 (2) 在标识符后面不识别“->”的基本行为。

对于成功编写 Parsec 语法的任何人来说,我的错误可能都很明显。谁能指出来?

【问题讨论】:

    标签: haskell f# parsec backtracking


    【解决方案1】:

    我认为问题在于,我正在为 F# 做一个假设(因为我不知道),箭头是正确关联的。我不确定链接语法应该有多精确,因为我不精通不同的语法。但是,如果我们可以假设箭头是右结合的,那么问题就会变得更容易。

    所以有了这个假设,我们可以轻松做到:

    identP = many1 (digit <|> letter <|> char '.' <|> char '`')
    
    typeP = try arrowP <|> identP
    
    arrowP = do
      i <- identP
      string "->"
      t <- typeP
      return $ "(" ++ i ++ " -> " ++ t ++ ")"
    
    run = flip parse "F# type syntax" $ do
            t <- typeP
            eof
            return t
    

    所以:

    Haskell> run "int"
    Right "int"
    Haskell> run "int->int"
    Right "(int -> int)"
    Haskell> run "int->int->int->int"
    Right "(int -> (int -> (int -> int)))"
    

    进一步扩展,您可能会感到困惑的是,在该语法中它说 type -> type,这意味着您可以在左侧有一个箭头。这很好,但它需要在括号中。这会有所帮助,也许看到以下内容会有所帮助。它帮助了我。

    typeP = try arrowP <|> parens typeP <|> identP
    
    arrowP = do
     i <- parens typeP <|> identP
     string "->"
     t <- typeP
     return $ "(" ++ i ++ " -> " ++ t ++ ")"
    
    parens p  = between (char '(') (char ')') p
    

    现在我们可以在箭头的左侧或右侧写箭头:

    Haskell> run "int->int->int"
    Right "(int -> (int -> int))"
    Haskell> run "(int->int)->int"
    Right "((int -> int) -> int)"
    

    【讨论】:

    • 很好的解释。正如您所注意到的,问题的根源在于您需要打破 arrowP 可以下降到 typeP 的循环,而 typeP 本身可以下降到 typeP。我认为您的parens 示例特别有启发性。
    • 因此 Parsec 语法与 LR(1) 语法基本上具有相同的非组合问题,因为您必须计划整个语法,以便每个规则的左边缘最终重写为明确的文字.哦,好吧,我想我应该知道的比假设 Parsec 是魔法更好。
    【解决方案2】:

    我认为您应该将左递归排除在语法之外。而不是

    type ::= identifier | type -> type 
    identifier ::= [A-Za-z0-9.`]+ 
    

    你会得到类似的东西

    typeStart ::= identifier 
    type ::= typeStart (-> type)?
    identifier ::= [A-Za-z0-9.`]+ 
    

    我认为这将更容易直接转换为秒差距。 (有人会认为try 会起作用,我希望它会以某种方式起作用,但是是的,我的经验也是,在我理解“将try 放在哪里”之前,我必须在 Parsec 中至少齐腰深让事情顺利进行。)

    还可以考虑查看Monadic Parser Combinators in F#(以及之前的 7 个 C# 博客条目)了解一些基础知识。我认为parsec docs(如果我没记错的话,试着从上到下阅读它们,它们是不错的)以及各种研究论文中的一些例子讨论了你问题中的问题。

    【讨论】:

    • 你是对的,研究论文可能是明确回答这个问题的最佳选择。我有implemented toy parser combinators,我只是没有用他们写过很多语法。我认为 Parsec 会神奇地比我用 Python 和 C# 编写的 hoky 示例更智能。
    【解决方案3】:

    这不会帮助您了解哪里出错了,但我建议您考虑使用sepBy1 来解析由-&gt; 符号分隔的类型。这将为您提供已解析类型的列表,然后您可以将其转换回函数类型。

    【讨论】:

    • 是的,我想我最终会这样做,但由于 sepBy1 可能涉及我必须手动编写的相同递归,我想我会从更简单的语法开始。
    • @Nathan - 是的,即使您使用 sepBy1,您仍然需要使用 Christopher 的方法来中断递归。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多