parsec:带有用错误消息的字符串选择解析器

时间:2015-12-18 13:19:50

标签: haskell parsec parser-combinators

让我们有以下解析器:

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"而不是我们的硬编码错误消息。

3 个答案:

答案 0 :(得分:1)

旧的非工作示例的旧答案

您安装了哪个版本的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"

添加的<?> error_message不会更改任何内容,只会将最后一行更改为expecting expected one of ['foo', 'fob', 'bar']

如何从Parsec中提取更多错误

因此,在这种情况下,您不应该信任错误消息,以便详尽了解系统中可用的信息。让我为Show提供一个时髦的Text.Parsec.Error:Message实例(基本上它是deriving (Show)的内容),以便您可以看到Parsec的内容:< / p>

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将其所有信息转储到一堆并行消息中,并存储&#34;意外的文件结尾&#34;为SysUnExpect ""show的{​​{1}}实例显然抓取了第一个ParseError但是所有SysUnExpect个消息并将其转储给您以供查看。

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

SysUnExpect

这可能值得重写或向上游发送错误,因为这有点奇怪,并且数据结构并不适合它们。首先,简而言之,问题是:似乎每个 showSysUnExpect | not (null unExpect) || null sysUnExpect = "" | null firstMsg = msgUnExpected ++ " " ++ msgEndOfInput | otherwise = msgUnExpected ++ " " ++ firstMsg 都应该有Message,而不是每个ParseError。

因此,有一个更早的步骤SourcePos,它更喜欢ParseErrors和更晚的mergeErrors - es。这并不会触发,因为消息不具有SourcePos,这意味着来自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'覆盖它。第三,可能导出ParseError构造函数;第四,<?>中的枚举类型真的很难看,可能更好地放入Message或其他东西,即使是现在的化身。 (例如,如果消息不是按特定顺序排列,则ParseError {systemUnexpected :: [Message], userUnexpected :: [Message], expected :: [Message], other :: [Message]}的当前Show会断断续续。)

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

答案 1 :(得分: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",并且在以{{1}开头的解析失败后要求il }:

ta

答案 2 :(得分:1)

这是我与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,但我们已经编写了自己的教程,而且我们的文档非常好。