【问题标题】:Parsec - error "combinator 'many' is applied to a parser that accepts an empty string"Parsec - 错误“组合器'许多'应用于接受空字符串的解析器”
【发布时间】:2011-10-13 12:16:29
【问题描述】:

我正在尝试使用 Parsec 编写一个解析器,该解析器将解析 literate Haskell 文件,例如:

The classic 'Hello, world' program.

\begin{code}

main = putStrLn "Hello, world"

\end{code}

More text.

受 RWH 中的示例启发,我编写了以下内容:

import Text.ParserCombinators.Parsec

main
    = do contents <- readFile "hello.lhs"
         let results = parseLiterate contents
         print results

data Element
    = Text String
    | Haskell String
    deriving (Show)


parseLiterate :: String -> Either ParseError [Element]

parseLiterate input
    = parse literateFile "(unknown)" input



literateFile
    = many codeOrProse

codeOrProse
    = code <|> prose

code
    = do eol
         string "\\begin{code}"
         eol
         content <- many anyChar
         eol
         string "\\end{code}"
         eol
         return $ Haskell content

prose
    = do content <- many anyChar
         return $ Text content

eol
    =   try (string "\n\r")
    <|> try (string "\r\n")
    <|> string "\n"
    <|> string "\r"
    <?> "end of line"

我希望这会产生以下结果:

[Text "The classic 'Hello, world' program.", Haskell "main = putStrLn \"Hello, world\"", Text "More text."]

(允许空格等)。

这编译正常,但运行时出现错误:

*** Exception: Text.ParserCombinators.Parsec.Prim.many: combinator 'many' is applied to a parser that accepts an empty string

任何人都可以对此有所了解,并可能提供解决方案吗?

【问题讨论】:

    标签: haskell parsec


    【解决方案1】:

    我没有测试过,但是:

    • many anyChar 可以匹配空字符串
    • 因此prose可以匹配一个空字符串
    • 因此codeOrProse可以匹配一个空字符串
    • 因此literateFile 可以永远循环,匹配无限多个空字符串

    prose 更改为匹配many1 字符可能会解决此问题。

    (我对 Parsec 不是很熟悉,但是 prose 怎么知道它应该匹配 多少 个字符?它可能会消耗整个输入,永远不会给 code 解析器一秒钟有机会寻找新代码段的开头。或者,它可能在每次调用中只匹配一个字符,使得其中的 many/many1 无用。)

    【讨论】:

    • 我肯定明白你在说什么,这就是为什么我试图将代码部分放在 中的散文部分之前。我想我想要一个不贪婪的“很多”。
    【解决方案2】:

    作为参考,这是我想出的另一个版本(稍微扩展以处理其他情况):

    import Text.ParserCombinators.Parsec
    
    main
        = do contents <- readFile "test.tex"
             let results = parseLiterate contents
             print results
    
    data Element
        = Text String
        | Haskell String
        | Section String
        deriving (Show)
    
    parseLiterate :: String -> Either ParseError [Element]
    
    parseLiterate input
        = parse literateFile "(unknown)" input
    
    literateFile
        = do es <- many elements
             eof
             return es
    
    elements
        = try section
      <|> try quotedBackslash
      <|> try code
      <|> prose
    
    code
        = do string "\\begin{code}"
             c <- anyChar `manyTill` try (string "\\end{code}")
             return $ Haskell c
    
    quotedBackslash
        = do string "\\\\"
             return $ Text "\\\\"
    
    prose
        = do t <- many1 (noneOf "\\")
             return $ Text t
    
    section
        = do string "\\section{"
             content <- many1 (noneOf "}")
             char '}'
             return $ Section content
    

    【讨论】:

      【解决方案3】:

      正如 sth 指出的那样,many anyChar 是问题所在。但不仅在prose 中,而且在code 中。 code 的问题在于,content &lt;- many anyChar 将消耗所有内容:换行符和 \end{code} 标记。

      因此,您需要有一些方法来区分散文和代码。一个简单(但可能太天真)的方法是寻找反斜杠:

      literateFile = many codeOrProse <* eof
      
      code = do string "\\begin{code}"
                content <- many $ noneOf "\\"
                string "\\end{code}"
                return $ Haskell content
      
      prose = do content <- many1 $ noneOf "\\"
                 return $ Text content
      

      现在,您并没有完全得到想要的结果,因为Haskell 部分也会包含换行符,但您可以很容易地过滤掉它们(给定一个函数filterNewlines,您可以说`content &lt;- filterNewlines &lt;$&gt; (many $ noneOf "\\"))。

      编辑

      好的,我想我找到了解决方案(需要最新的 Parsec 版本,因为lookAhead):

      import Text.ParserCombinators.Parsec
      import Control.Applicative hiding (many, (<|>))
      
      main
          = do contents <- readFile "hello.lhs"
               let results = parseLiterate contents
               print results
      
      data Element
          = Text String
          | Haskell String
          deriving (Show)    
      
      parseLiterate :: String -> Either ParseError [Element]
      
      parseLiterate input
          = parse literateFile "" input
      
      literateFile
          = many codeOrProse
      
      codeOrProse = code <|> prose
      
      code = do string "\\begin{code}\n"
                c <- untilP (string "\\end{code}\n")
                string "\\end{code}\n"
                return $ Haskell c
      
      prose = do t <- untilP $ (string "\\begin{code}\n") <|> (eof >> return "")
                 return $ Text t
      
      untilP p = do s <- many $ noneOf "\n"
                    newline
                    s' <- try (lookAhead p >> return "") <|> untilP p
                    return $ s ++ s'
      

      untilP p解析一行,然后检查下一行的开头是否可以被p成功解析。如果是,则返回空字符串,否则继续。 lookAhead 是必需的,否则会消耗 begin\end-tags 并且 code 无法识别它们。

      我想它仍然可以更简洁(即不必在code 中重复string "\\end{code}\n")。

      【讨论】:

      • 问题是代码和散文都可以包含反斜杠(代码是因为 lambdas 等,而散文是因为它可以包含 TeX 命令)。
      • (唯一的区别是 \begin{code} 和 \end{code} 必须在换行符上)。
      • 我明白了 - 没有考虑清楚......我想你必须以某种方式重构你的语法,以便它在每个换行符之后检查下一部分是否是 \begin{code}或 \end{code} 字符串。很遗憾。我对语法不是很有经验。
      • 太棒了,谢谢!我知道try,但之前没见过lookAhead
      • untilP 不允许该部分为空(它必须在开始和结束之间至少包含一个换行符)。允许一个空的部分,untilP smanyTill (manyTill anyChar newline) (try s) 不一样,不需要再次啜食终结符吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-15
      • 2015-07-12
      • 2016-02-20
      相关资源
      最近更新 更多