【问题标题】:parsec: string choice parser with useful error messagesparsec:带有有用错误消息的字符串选择解析器
【发布时间】:2015-12-18 13:19:50
【问题描述】:

让我们有以下解析器:

parser :: GenParser Char st String
parser = choice (fmap (try . string) ["head", "tail", "tales"]
                    <?> "expected one of ['head', 'tail', 'tales']")

当我们解析格式错误的输入“ta”时,它将返回定义的错误,但由于回溯,它还会在第一个位置谈论unexpected "t",而不是在位置3 谈论unexpected " "

是否有一种简单(或内置)的方法来匹配多个预期字符串中的一个,从而产生良好的错误消息?我说的是显示正确的位置,在这种情况下类似于expected "tail" or "tales",而不是我们硬编码的错误消息。

【问题讨论】:

  • 你可以写your own choice operator
  • 我不认为这很容易。仅仅使用&lt;|&gt; 是行不通的,因为这正是choice 正在做的事情。我需要逐个检查 Char 并消除每个步骤中不可能的选择。我问,因为这似乎是一个普遍的问题。奇怪的是,如果没有标准解决方案。
  • 啊,看来你的问题的区别和原因是try
  • 是的,它正在执行我在初始帖子中提到的回溯。 choice 需要它,并且在使用 &lt;|&gt; 通过单个解析器时也需要它。没有它,失败的解析器将消耗输入。

标签: haskell parsec parser-combinators


【解决方案1】:

编写一个正确执行此操作的函数并不难。我们一次只撕掉一个字符,使用Data.Map 来查找共享后缀:

{-# LANGUAGE FlexibleContexts #-}
import Control.Applicative
import Data.Map hiding (empty)
import Text.Parsec hiding ((<|>))
import Text.Parsec.Char

-- accept the empty string if that's a choice
possiblyEmpty :: Stream s m Char => [String] -> ParsecT s u m String
possiblyEmpty ss | "" `elem` ss = pure ""
                 | otherwise    = empty

chooseFrom :: Stream s m Char => [String] -> ParsecT s u m String
chooseFrom ss
     =  foldWithKey (\h ts parser -> liftA2 (:) (char h) (chooseFrom ts) <|> parser)
                    empty
                    (fromListWith (++) [(h, [t]) | h:t <- ss])
    <|> possiblyEmpty ss

我们可以在 ghci 中验证它是否成功匹配 "tail""tales",并且在以 ta 开头的解析失败后请求 il

*Main> parse (chooseFrom ["head", "tail", "tales"]) "" "tail"
Right "tail"
*Main> parse (chooseFrom ["head", "tail", "tales"]) "" "tales"
Right "tales"
*Main> parse (chooseFrom ["head", "tail", "tales"]) "" "tafoo"
Left (line 1, column 3):
unexpected "f"
expecting "i" or "l"

【讨论】:

    【解决方案2】:

    旧的非工作示例的旧答案

    您安装了哪个版本的parsec? 3.1.9 为我做这个:

    Prelude> :m + Text.Parsec Text.Parsec.String
    Prelude Text.Parsec Text.Parsec.String> :set prompt Main>
    Main> let parser = choice (map (try . string) ["foo", "fob", "bar"]) :: GenParser Char st String
    Main> runParser parser () "Hey" "fo "
    Left "Hey" (line 1, column 1):
    unexpected " "
    expecting "foo", "fob" or "bar"
    Main> runParser parser () "Hey" "fo"
    Left "Hey" (line 1, column 1):
    unexpected end of input
    expecting "foo", "fob" or "bar"
    

    添加的&lt;?&gt; error_message 没有任何改变,只是将最后一行更改为expecting expected one of ['foo', 'fob', 'bar']

    如何从 Parsec 中提取更多错误

    因此,在这种情况下,您不应相信错误消息会详尽地说明系统中可用的信息。让我为Text.Parsec.Error:Message 提供一个时髦的Show 实例(基本上就是deriving (Show) 的情况),这样你就可以看到Parsec 的结果:

    Main> :m + Text.Parsec.Error
    Main> instance Show Message where show m = (["SysUnExpect", "UnExpect", "Expect", "Message"] !! fromEnum m) ++ ' ' : show (messageString m)
    Main> case runParser parser () "" "ta" of Left pe -> errorMessages pe
    [SysUnExpect "\"t\"",SysUnExpect "",SysUnExpect "",Expect "\"head\"",Expect "\"tail\"",Expect "\"tales\""]
    

    您可以看到秘密 choice 正在将其所有信息转储到一堆并行消息中,并将“意外的文件结尾”存储为SysUnExpect ""ParseErrorshow 实例显然获取了第一个 SysUnExpect 但所有 Expect 消息并转储它们供您查看。

    目前执行此操作的确切函数是Text.Parsec.Error:showErrorMessages。错误消息应该是有序的,并根据构造函数分为 4 个块; SysUnExpect 块是通过一个特殊的显示函数发送的,如果有真正的 UnExpect 元素,它会完全隐藏文本,否则只显示第一个 SysUnExpect 消息:

      showSysUnExpect | not (null unExpect) ||
                        null sysUnExpect = ""
                      | null firstMsg    = msgUnExpected ++ " " ++ msgEndOfInput
                      | otherwise        = msgUnExpected ++ " " ++ firstMsg
    

    可能值得重写或向上游发送错误,因为这是一种奇怪的行为,并且数据结构不太适合它们。首先,简而言之,您的问题是:似乎每个Message 都应该有一个SourcePos,而不是每个 ParseError。

    所以,有一个更早的步骤,mergeErrors,它更喜欢 ParseErrors 和后来的SourcePos-es。这不会触发,因为消息没有SourcePos,这意味着来自choice 的所有错误都从字符串的开头开始,而不是从匹配的最大点开始。例如,您可以在解析 "tai" 时不会卡住这一点:

    let parser = try (string "head") <|> choice (map (try . (string "ta" >>) . string) ["il", "les"]) :: GenParser Char st Strinh
    

    其次,除此之外,也许我们应该将一起发送的消息绑定在一起(所以默认消息是unexpected 't', expected "heads" | unexpected end-of-file, expected 'tails' | unexpected end-of-file, expected 'tales',除非你用&lt;?&gt; 覆盖它)。第三,可能 ParseError 构造函数应该被导出;第四,Message 中的枚举类型真的很难看,最好放在ParseError {systemUnexpected :: [Message], userUnexpected :: [Message], expected :: [Message], other :: [Message]} 或其他东西中,即使是现在的化身。 (例如,如果消息没有按特定顺序排列,ParseError 的当前 Show 将巧妙地中断。)

    与此同时,我建议您为 ParseError 编写自己的 show 变体。

    【讨论】:

    • 相同版本。你是对的:这行得通。我的例子很糟糕。我编辑了这个问题,它现在使用不同的字符串。我不知道为什么第一个示例确实有效,可能是因为所有字符串的长度相同。
    • @snøreven 添加了另一个可能有帮助的部分。
    • 非常感谢您对问题的详细分析。我提交了一个错误并在此处引用了您的答案。
    【解决方案3】:

    这是我对 Parsec 的了解:

    λ> let parser = choice $ fmap (try . string) ["head", "tail", "tales"]
    λ> parseTest parser "ta"
    parse error at (line 1, column 1):
    unexpected "t"
    expecting "head", "tail" or "tales"
    

    如果您想尝试现代版本的 Parsec — Megaparsec,你会结束的 最多:

    λ> let parser = choice $ fmap (try . string) ["head", "tail", "tales"]
    λ> parseTest parser "ta"
    1:1:
    unexpected "ta" or 't'
    expecting "head", "tail", or "tales"
    

    这里发生了什么?首先,当我们解析有序的字符集合时, 与string 一样,我们完全显示不正确的输入。这是很多 我们认为更好,因为:

    λ> parseTest (string "when" <* eof) "well"
    1:1:
    unexpected "we"
    expecting "when"
    

    我们指向单词的开头,并显示整个内容 不正确(直到第一个不匹配的字符)和整个事情我们 预计。在我看来,这更具可读性。仅基于 tokens 构建的解析器 以这种方式工作(也就是说,当我们尝试匹配固定字符串时, 不区分大小写的变体可用)。

    那么,unexpected "ta" or 't' 呢,为什么我们会得到't' 部分?这是 也绝对正确,因为通过您收集的替代方案, 第一个字母't' 本身也可能出乎意料,因为您有 不以't' 开头的替代方案。再看一个例子:

    λ> let parser = choice $ fmap (try . string) ["tall", "tail", "tales"]
    λ> parseTest parser "ta"
    1:1:
    unexpected "ta"
    expecting "tail", "tales", or "tall"
    

    或者怎么样:

    λ> parseTest (try (string "lexer") <|> string "lexical") "lex"
    1:1:
    unexpected "lex"
    expecting "lexer" or "lexical"
    

    秒差距:

    λ> parseTest (try (string "lexer") <|> string "lexical") "lex"
    parse error at (line 1, column 1):
    unexpected end of input
    expecting "lexical"
    

    既然它可以“正常工作”,为什么还要努力让它发挥作用?

    Megaparsec 还有许多其他很棒的东西,如果您有兴趣, 你可以了解更多 here。很难与之竞争 Parsec,但我们已经编写了自己的教程并且我们的文档非常好。

    【讨论】:

    • 有趣,不知道还有另一个解析器库。问题:您的文档链接到不存在的教程(页面上的链接链接到自身)。我是 Haskell 的新手,希望看到一些示例用法(带有类型签名和所有内容)。
    • @snøreven,底部有菜单。抱歉,我将不得不移动它,因为它不是很明显。这是直接链接:mrkkrp.github.io/megaparsec/tutorials.html.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-22
    • 1970-01-01
    • 1970-01-01
    • 2021-11-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多