我们需要的是解析器。这只是一个将字符串作为输入并返回数据结构作为输出的函数。我将向您展示一种以“组合器”样式创建解析器的简化方法。这意味着我们将从更小的解析器中构建我们想要的解析器(通过组合它们)。
这不是最好或最有效的方法,但它会展示这项技术。而且它不需要任何库!
我们将从语言编译指示开始,以减少一些样板:
{-# LANGUAGE DeriveFunctor #-}
现在让我们创建一个数据类型来表示解析函数。
data Parser a = P { parser :: String -> Maybe (String, a) } deriving Functor
基本上,解析器是数据包装器下的一个函数。它的工作方式是将一个字符串作为输入,如果它的条件与字符串开头的字符匹配,那么它将使用这些字符,创建a 类型的数据并返回一个包含未使用的Just输入和新项目。但是,如果条件失败,那么它只会返回 Nothing。
我们将为 Parser 类型实现 Applicative 和 Monad,然后我们将能够使用 do 表示法。这是 Haskell(恕我直言)最酷的功能之一。我们不会使用 Applicative <*>,但我们需要实例来实现 Monad。 (尽管 Applicative 本身就很棒。)
instance Applicative Parser where
pure x = P (\input -> Just (input, x))
f <*> p = do
f' <- f
p' <- p
return $ f' p'
Monad 需要的关键操作是绑定 (>>=),它获取第一个解析器的结果并将其提供给返回第二个解析器的函数。这是组合解析器最方便的方法。它让我们无需通过解析器函数手动线程化输入即可累积(或丢弃)结果。
instance Monad Parser where
return = pure
p >>= f = P (\input -> case parse p input of
Just (rest, x) -> parse (f x) rest
_ -> Nothing)
接下来我们需要一种方法来创建“原始”解析器。我们将创建一个函数,它接受一个Char 谓词并返回一个解析器,该解析器将接受一个通过谓词的字符。
satisfy :: (Char -> Bool) -> Parser Char
satisfy p = P (\input -> case input of
(x:xs) | p x -> Just (xs, x) -- success!
_ -> Nothing -- failure :(
还有很多其他方法可以操纵解析器,但我们会坚持解决给定的问题。接下来我们需要的是一种重复解析器的方法。这就是while 函数派上用场的地方。它需要一个解析器来生成 a 类型的项目并重复它直到它失败,并将结果累积到一个列表中。
while :: Parser a -> Parser [a]
while p = P (\input -> case parse p input of
Nothing -> Just (input, [])
Just (rest, x) -> parse (fmap (x:) (while p)) rest)
我们快完成了。我们将创建谓词来区分空格和非空格。
isWhitespace c = c == ' ' || c == '\t' || c == '\n'
isNotWhiteSpace = not . isWhitespace
好的,现在我们来看看 do-notation 有多棒。首先,我们为单个单词创建一个解析器。
word :: Parser String
word = do
c <- (satisfy isNotWhitespace) -- grab the first character
cs <- while (satisfy isNotWhitespace) -- get any other characters
while (satisfy isWhitespace) -- eat the trailing whitespace
return (c:cs)
我们终于可以实现我们真正想要的解析器了!
splitWords :: Parser [String]
splitWords = do
while (satisfy isWhitespace) -- eat up any leading whitespace
while word
最后,试试吧!
main :: IO ()
main = do
let input = " A \t String with many\nspaces."
case parse splitWords input of
Nothing -> putStrLn "failed!"
Just (_, result) -> putStrLn . show $ result
这是我在 ghci 中得到的:
λ main
["A","String","with","many","spaces."]