为什么看起来Parsec Choice运算符依赖于解析器的顺序?

时间:2015-10-10 18:25:15

标签: haskell parsec

我试图解析一个只包含十进制或二进制数的非常简单的语言。例如,以下是一些有效的输入:

#b1
#d1
#b0101
#d1234

我在使用Parsec的选择运算符时遇到问题:<|>。根据教程:Write yourself a Scheme in 48 hours

  

[选择运算符]尝试第一个解析器,如果失败,则尝试第二个解析器。如果成功,则返回该解析器返回的值..

但根据我的经验,我发现解析器的顺序很重要。这是我的计划:

import System.Environment
import Text.ParserCombinators.Parsec

main :: IO ()
main = do
  (x:_) <- getArgs 
  putStrLn ( "Hello, " ++ readExp x)

bin :: Parser String
bin = do string "#b"
         x <- many( oneOf "01")
         return x

dec :: Parser String
dec = do string "#d"
         x <- many( oneOf "0123456789")
         return x

-- Why does order matter here?
parseExp = (bin <|> dec) 

readExp :: String -> String
readExp input = case parse parseExp "test" input of
                      Left error -> "Error: " ++ show error
                      Right val  -> "Found val" ++ show val 

以下是我运行该程序的方法:

安装依赖项

$ cabal sandbox init
$ cabal install parsec

编译

$ cabal exec ghc Main

运行

$ ./Main "#d1"
Hello, Error: "test" (line 1, column 1):
unexpected "d"
expecting "#b"

$ ./Main "#b1"
Hello, Found val"1"

如果我按如下方式更改解析器的顺序:

parseExp = (dec <|> bin) 

然后只检测到二进制数,程序无法识别十进制数。

通过我已执行的测试,我发现只有当其中一个解析器开始解析输入时,才会发生此问题。如果找到散列字符#,则激活bin解析器,最终失败,因为预期的下一个字符是b而不是d。似乎应该会发生一些应该发生的回溯,我不知道。

感谢帮助!

2 个答案:

答案 0 :(得分:7)

Parsec有两种“失败”:存在消耗输入的故障,而没有消耗输入的故障。为了避免回溯(因此保持输入超过必要的时间/通常对垃圾收集器不友好),(<|>)一旦消耗输入就立即提交给第一个解析器;因此,如果它的第一个参数消耗输入并失败,它的第二个解析器永远不会有机会成功。您可以使用try明确请求回溯行为,因此:

Text.Parsec> parse (string "ab" <|> string "ac") "" "ac"
Left (line 1, column 1):
unexpected "c"
expecting "ab"
Text.Parsec> parse (try (string "ab") <|> string "ac") "" "ac"
Right "ac"

不幸的是,try有一些令人讨厌的性能损失,这意味着如果你想要一个高性能的解析器,你将不得不重构一下你的语法。我会用上面的解析器这样做:

Text.Parsec> parse (char 'a' >> (("ab" <$ char 'b') <|> ("ac" <$ char 'c'))) "" "ac"
Right "ac"

在您的情况下,您需要同样分解“#”标记。

答案 1 :(得分:3)

仔细阅读:

  

[选择运算符]尝试第一个解析器,如果失败则尝试   第二。如果成功,则返回返回的值   那个解析器..

这意味着首先尝试第一个解析器,如果成功,则根本不尝试第二个解析器!这意味着第一个解析器具有更高的优先级,因此<|>通常不可交换。

一个简单的反例可以用一些总是成功的解析器来制作,例如

dummy :: Parser Bool
dummy = return True <|> return False

以上相当于return True:因为第一个成功,第二个是无关紧要的。

最重要的是,parsec设计为在第一个分支成功消耗了一些输入时提前提交。这种设计牺牲了完美的非确定性以提高效率。否则,parsec通常需要将所有正在解析的文件保留在内存中,因为如果发生解析错误,则需要尝试一些新的替代方法。

例如,

p = (char 'a' >> char 'b' >> ...) <|>
    (char 'a' >> char 'c' >> ...)

无法像预期的那样发挥作用。只要第一个分支成功使用'a',parsec就会对其进行提交。这意味着如果'c'跟随,则第一个分支将失败,但是第二个分支的尝试为时已晚。整个解析器都失败了。

要解决此问题,可以将公共前缀分解出来,例如

p = char 'a' >> ( (char 'b' >> ...) <|> (char 'c' >> ...) )

或诉诸try

p = (try (char 'a' >> char 'b') >> ...) <|>
    (char 'a' >> char 'c' >> ...)

try基本上告诉parsec在try下的整个解析器成功之前不要提交分支。如果被滥用,它可能导致parsec将大部分文件保留在内存中,但用户至少对此有一些控制权。从理论上讲,通过始终将try添加到<|>的整个左侧分支,可以恢复完美的非确定性。但是不建议这样做,因为它会刺激程序员编写一个低效的解析器,因为内存问题以及人们可以轻松编写需要指数数量的回溯来成功解析的语法。