解析Haskell自定义数据类型

Jon*_*ans 2 parsing haskell parsec attoparsec

我已经通过这里提供的Haskell Koans工作:https: //github.com/roman/HaskellKoans

我被困在最后两个Koans上,都涉及解析自定义代数数据类型.这是第一个:

data Atom = AInt Int | ASym Text deriving (Eq, Show)

testAtomParser :: Test
testAtomParser = testCase "atom parser" $ do
    -- Change parser with the correct parser to use
    --
    let parser = <PARSER HERE> :: P.Parser Atom
    assertParse (ASym "ab") $ P.parseOnly parser "ab"
    assertParse (ASym "a/b") $ P.parseOnly parser "a/b"
    assertParse (ASym "a/b") $ P.parseOnly parser "a/b c"
    assertParse (AInt 54321) $ P.parseOnly parser "54321"
Run Code Online (Sandbox Code Playgroud)

如何定义变量解析器,以便它可以解析代数数据类型Atom以传递断言?

J. *_*son 9

一世.

ADT的解析器倾向于反映ADT的形状.你的ADT是由两个不相交的部分组成的,所以你的解析器也可能有两个不相交的部分

atom = _ <|> _
Run Code Online (Sandbox Code Playgroud)

II.

假设我们知道如何解析单个数字(让我们称之为基本解析器digit),那么我们通过重复它来解析(非负)整数.

natural = let loop = digit >> loop in loop
Run Code Online (Sandbox Code Playgroud)

这成功地解析了无限的数字流并将它们抛弃.我们可以做得更好吗?不幸的是,不仅仅是monad实例,我们需要另一个基本的组合器,many它修改一些其他解析器以消耗输入0次或更多次,将结果累积到列表中.我们实际上会稍微调整一下,因为空解析不是有效数字

many1 p = do x  <- p
             xs <- many p
             return (x:xs)

natural' = many1 digit
Run Code Online (Sandbox Code Playgroud)

III.

原子怎么样?为了传递测试用例,似乎原子必须是1对多的字母数字字符反斜杠.同样,这个不相交的结构可以立即在我们的解析器中表达

sym = many1 (_ <|> _)
Run Code Online (Sandbox Code Playgroud)

我们将再次使用一些内置的简单解析器组合来构建我们想要的东西,比如satisfy :: (Char -> Bool) -> Parser Char匹配满足某些谓词的任何字符.我们可以立即建立另一个有用的组合器,char c = satisfy (==c) :: Char -> Parser Char然后我们就完成了.

sym = many1 (char '/' <|> satisfy isAlpha)
Run Code Online (Sandbox Code Playgroud)

isAlpha谓词在哪里很像正则表达式[a-zA-Z].

IV.

所以现在我们有了解析器的核心

natural <|> sym :: Parser String
Run Code Online (Sandbox Code Playgroud)

many1组合子举起我们的性格解析器到的解析器列表的字符(String小号!).这个提升动作也是构建ADT解析器的基本思路.我们想把我们Parser String提升到Parser Atom.一种方法是使用toAtom :: String -> Atom我们可以fmap进入的函数Parser

atom' :: Parser Atom
atom' = fmap toAtom (natural <|> sym)
Run Code Online (Sandbox Code Playgroud)

但是具有类型的函数首先会String -> Atom破坏构建解析器的目的.

如I.中所述,重要的部分是ADT的形状反映在我们的atom解析器的形状中.我们需要利用它来构建我们的最终解析器.

V.

我们需要利用atom解析器结构中的信息.让我们构建两个函数

liftInt :: String -> Atom  -- creates `AInt`s
liftSym :: String -> Atom  -- creates `ASym`s

liftInt = AInt . read
liftSym = ASym
Run Code Online (Sandbox Code Playgroud)

每一个都说明了将Strings变成Atoms 的方法,但也宣告了我们正在处理什么的东西Atom.值得注意的是,liftInt如果我们传递一个无法解析为的字符串,则会抛出运行时错误Int.幸运的是,这正是我们所知道的.

atomInt :: Parser Atom
atomInt = liftInt <$> natural

atomSym :: Parser Sym
atomSym = liftSym <$> sym

atom'' = atomInt <|> atomSym
Run Code Online (Sandbox Code Playgroud)

现在,我们的atom''解析器利用的保证是natural只返回字符串这是一个自然的---我们呼吁有效解析到read不会失败!---我们尝试建立既AIntASym才能,在非连续的尝试后,一个又一个结构就像我们ADT的结构一样.

VI.

整个社会就是这样

atom''' =     AInt . read <$> many1 digit
          <|> ASym <$> many1 (    char '/' 
                              <|> satisfy isAlpha)
Run Code Online (Sandbox Code Playgroud)

这展示了解析器组合器的乐趣.整个事物是使用小巧,可组合的简单部件从地面构建的.每个人都做了一个非常小的工作,但他们一起跨越了大量的解析器.

您还可以使用ADT中的更多分支,更完整指定的符号类型解析器或故障装饰来轻松扩充此语法,<?>以便在失败的分析中包含大量错误消息.