【问题标题】:Writing a parser from scratch in Haskell在 Haskell 中从头开始编写解析器
【发布时间】:2013-12-18 14:26:43
【问题描述】:

有没有什么好的教程可以从零开始在 Haskell 中为给定的语法编写解析器?

我找到了:

  1. parsing expressions and statements (HaskellWiki)

  2. Parsing a simple imperative language (HaskellWiki)

  3. Using parsec (Real World Haskell)

但所有这些都使用 parsec 库,虽然这对于工业应用程序可能很有趣,但我专门寻找“自下而上”而不使用复杂库的示例。

我发现唯一一个使用“基本”Haskell 的是:Parsing with Haskell 它使用了一些非常陌生的语法(很难区分程序的一部分或仅是“伪代码”),并且没有明确的语法定义。

非常感谢任何建议!

【问题讨论】:

  • 我发现 parsec 用户手册很有帮助。 legacy.cs.uu.nl/daan/parsec.html
  • 确实很有帮助,我现在正在使用它,对这个主题有一种感觉很好,但是正如我提到的从头开始实现解析器的完整教程/示例将很高兴看到“引擎盖下”。
  • Jeroen Fokker 的 Functional Parsers 值得一读。
  • 与 Parsec 手册一样,来自 Daan Leijen 遗留页面的 Parsec 的原始分发版包含两种小语言的完整示例解析器 - Henk 和 Mondrian。

标签: parsing haskell


【解决方案1】:

从头开始构建 Parsec 实际上非常容易。实际的库代码本身是高度概括和优化的,这会扭曲核心抽象,但如果您只是从头开始构建东西以了解更多关于正在发生的事情,您只需几行代码即可编写它。我将在下面构建一个稍弱的Applicative 解析器。

本质上,我们想要生成ApplicativeParser 以及原始解析器值

satisfy :: (Char -> Bool) -> Parser Char

还有一些组合符,例如 try,如果解析器失败,它们会“撤消”解析器

try :: Parser a -> Parser a

orElse 如果第一个解析器失败,我们可以继续使用第二个解析器。通常这实际上是用中缀组合符(<|>)

orElse, (<|>) :: Parser a -> Parser a -> Parser a

由于我们的Applicative 需要跟踪当前流状态并且能够失败,我们将通过结合状态ApplicativeEither 应用程序来构建它。

type Error = String
newtype Parser a = P { unP :: String -> (String, Either Error a) }

instance Functor Parser where
  fmap f (P st) = P $ \stream -> case st stream of
    (res, Left err) -> (res, Left err)
    (res, Right a ) -> (res, Right (f a))

instance Applicative Parser where
  pure a = P (\stream -> (stream, Right a))
  P ff <*> P xx = P $ \stream0 -> case ff stream0 of   -- produce an f
    (stream1, Left err) -> (stream1, Left err)
    (stream1, Right f ) -> case xx stream1 of          -- produce an x
      (stream2, Left err) -> (stream2, Left err)
      (stream2, Right x ) -> (stream2, Right (f x))    -- return (f x)

如果我们仔细遵循Applicative 实例中的(&lt;*&gt;) 方法,我们会看到它只是将流传递到f-生成Parser,获取结果流,如果成功,将其传递给x-produce Parser 如果他们都成功了,则返回他们的应用程序(f x)。这意味着如果我们有一个产生函数的解析器和一个产生参数的解析器,我们可以用(&lt;*&gt;)

对它们进行排序
-- given
parseChar :: Char -> Parser Char

parseHi   :: Parser (Char, Char) -- parses 'h' then 'i'
parseHi = pure (,) <$> parseChar 'h' <*> parseChar 'i'

我们也可以使用Applicative 的机制来构建所需的组合器。这里是satisfy

-- | Peek at the next character and return successfully if it satisfies a predicate
satisfy :: (Char -> Bool) -> Parser Char
satisfy f = P $ \stream -> case stream of
  []                 -> ([], Left "end of stream")
  (c:cs) | f c       -> (cs, Right c)
         | otherwise -> (cs, Left "did not satisfy")

这里是try

-- | Run a parser but if it fails revert the stream to it's original state
try :: Parser a -> Parser a
try (P f) = P $ \stream0 -> case f stream0 of
  (_      , Left err) -> (stream0, Left err)
  (stream1, Right a ) -> (stream1, Right a )

这里是orElse

orElse :: Parser a -> Parser a -> Parser a
orElse (P f1) (P f2) = P $ \stream0 -> case f1 stream0 of
  (stream1, Left err) -> f2 stream1
  (stream1, Right a ) -> (stream1, Right a)

通常在这一点上,如果我们还提供一个立即失败的解析器 emptyParser 将与 orElse 形成一个 Alternative 实例

instance Alternative Parser where
  empty = P $ \stream -> (stream, Left "empty")
  (<|>) = orElse

  many = manyParser
  some = someParser

我们可以将manyParsersomeParser 编写为重复运行解析器的组合子。

-- | 0 or more
manyParser :: Parser a -> Parser [a]
manyParser (P f) = P go where
  go stream = case f stream of
    (_      , Left err) -> (stream, Right [])  -- throws away the error
    (stream', Right a ) -> case go stream' of 
      (streamFin, Left err) -> (streamFin, Left err)
      (streamFin, Right as) -> (streamFin, Right (a : as))

-- | 1 or more
someParser :: Parser a -> Parser [a]
someParser (P f) = P $ \stream -> case f stream of
  (stream', Left err) -> (stream', Left err)
  (stream', Right a ) -> 
    let (P fmany) = manyParser (P f)
    in case fmany stream' of
      (stream'', Left err) -> (stream'', Left err)
      (stream'', Right as) -> (stream'', Right (a:as))

从这里我们可以开始在更高层次的抽象上工作。

char :: Char -> Parser Char
char c = satisfy (== c)

string :: String -> Parser String
string []     = pure []
string (c:cs) = (:) <$> char c <*> string cs

oneOf :: [Char] -> Parser Char
oneOf cs = satisfy (`elem` cs)

parens :: Parser a -> Parser a
parens parseA = dropFirstAndLast <$> char '(' <*> parseA <*> char ')'
  where
    dropFirstAndLast _ a _ = a

【讨论】:

  • orElse 函数中可能有错字 - 应该有一个 f2 stream0 调用。我对吗?顺便说一句,很好的答案-谢谢!!!
  • 取决于你想如何进行回溯。您可以使用try 构建您想要的内容,以限制回溯发生的位置。不同的设计对回溯语义做出不同的选择。
【解决方案2】:

我真的很喜欢 Graham Hutton 的“Haskell 编程”。它温和地介绍了 monad 和解析器组合器。第八章构建一个基本的解析器库。

这里是链接to Programming in Haskell book site。您还将找到指向解析器库和基本表达式解析器的链接。

此外,如果您有兴趣,还可以查看在乌得勒支大学开发的uu-parsinglib 应用样式解析器组合器。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-09-08
    • 2010-09-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-03
    • 1970-01-01
    相关资源
    最近更新 更多