使用Maybe进行错误检测和报告

时间:2013-06-12 00:47:12

标签: parsing haskell text-parsing

我正在Haskell中编写一个命题逻辑解析器。我现在正在手工解析作为学习练习。最终我将解决Parsec。与此同时,我正试图绕着Monads。特别是,我使用Maybe来报告parse函数中的错误。我目前的麻烦在于辅助函数的一部分:

parse' :: String -> (Maybe Wff, String)
parse' ('[':rest) = (x, if null rest''
                        then ""
                        else tail rest'')
        where (a, rest') = parse' rest
              (b, rest'') = parse' (if null rest'
                                    then ""
                                    else tail rest')
              x = if null rest'
                     || null rest''
                     || head rest' /= '|'
                     || head rest'' /= ']'
                  then Nothing
                  else Or <$> a <*> b

(作为参考,可以找到完整的parse函数here。)

此代码解析[ A | B ]形式的命题,其中AB是任意命题。如您所见,如果先前的递归调用导致Nothing,我在最后一行使用applicative样式来传播Nothing结果。这样我就可以从a == Nothing条件中取出b == Nothingif。如何使用ApplicativeMonad Maybe实例来完成其余if

3 个答案:

答案 0 :(得分:5)

我实际上会向后解决问题:我将从monadic解决方案开始,然后从它向后工作到手动解决方案。如果您手动找到正确的解决方案,这将生成相同的代码。

monadic解析器的典型类型签名具有以下形式:

type Parser a = String -> Maybe (a, String)

请注意与您的表单略有不同,String外部的最终Maybe。两者都有效,但我更喜欢这种形式,因为如果解析失败,我认为剩余的String无效。

此类型实际上是StateT的特例,其定义为:

newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }

请注意,如果我们选择:

s = String
m = Maybe

...我们返回Parser类型:

type Parser a = StateT String Maybe a

-- or: type Parser = StateT String Maybe

这有什么好处,我们只需要手动定义一个解析器,它是检索单个字符的解析器:

anyChar :: Parser Char
anyChar = StateT $ \str -> case str of
    []   -> Nothing
    c:cs -> Just (c, cs)

请注意,如果我们删除了StateT包装器,anyChar的类型将为:

anyChar :: String -> Maybe (Char, String)

当我们将它包裹在StateT中时,它变为:

anyChar :: StateT String Maybe Char

...这只是Parser Char

一旦我们有了这个原始解析器,我们就可以使用StateT的{​​{1}}接口来定义所有其他解析器。例如,让我们定义一个匹配单个字符的解析器:

Monad

这很简单! import Control.Monad char :: Char -> Parser () char c' = do c <- anyChar guard (c == c') 我们的monad需要guard个实例,但我们已经拥有了一个。原因是由于以下两个MonadPlus实例:

MonadPlus

这两个实例的组合意味着instance (MonadPlus m) => MonadPlus (StateT s m) where ... instance MonadPlus Maybe where ... 会自动实现StateT s Maybe,因此我们可以使用MonadPlus,它只会神奇地做“正确的事情”。

掌握了这两个解析器后,您的最终解析器变得非常容易编写:

guard

更清楚,更容易理解发生了什么。它也有效:

data Wff = Or Char Char deriving (Show)

parseWff :: Parser Wff
parseWff = do
    char '['
    a <- anyChar
    char '|'
    b <- anyChar
    char ']'
    return (Or a b)

向后工作

这将我们带到您原来的问题:您如何手写相同的行为?我们将从>>> runStateT parseWff "[A|B]" Just (Or 'A' 'B',"") Monad个实例向后工作,以便为我们推断出他们在做什么。

为此,我们必须推断出MonadPlus的{​​{1}}和Monad个实例减少到基本monad为MonadPlus的时间。让我们从StateT的{​​{1}}实例开始:

Maybe

请注意,它是以基本monad的一般术语定义的。这意味着我们还需要Monad的{​​{1}}实例来推导上述代码的作用:

StateT

如果我们将instance (Monad m) => Monad (StateT s m) where return r = StateT (\s -> return (r, s)) m >>= f = StateT $ \s0 -> do (a, s1) <- runStateT m s0 runStateT (f a) s1 monad实例替换为Monad monad实例,我们得到:

Maybe

我们可以为instance Monad Maybe where return = Just m >>= f = case m of Nothing -> Nothing Just a -> f a 派生Maybe实例做同样的事情。我们只需要为StateT和`Maybe:

instance Monad (StateT s Maybe) where return r = StateT (\s -> Just (r, s)) m >>= f = StateT $ \s0 -> case runStateT m s0 of Nothing -> Nothing Just (a, s1) -> runStateT (f a) s1 个实例
Monad

......并将它们组合成一个:

StateT s Maybe

现在我们可以得出我们的解析器正在做什么。让我们从MonadPlus解析器开始:

StateT

这令人厌恶:

instance (MonadPlus m) => MonadPlus (StateT s m) where
    mzero = StateT (\_ -> mzero)
    mplus (StateT f) (StateT g) = StateT (\s -> mplus (f s) (g s))

instance MonadPlus Maybe where
    mzero = Nothing
    mplus m1 m2 = case m1 of
        Just a  -> Just a
        Nothing -> case m2 of
            Just b  -> Just b
            Nothing -> Nothing

我们刚刚推导出instance MonadPlus (StateT s Maybe) where mzero = StateT (\_ -> Nothing) mplus (StateT f) (StateT g) = StateT $ \s -> case f s of Just a -> Just a Nothing -> case g s of Just b -> Just b Nothing -> Nothing char所做的事情,所以我们将其替换为:

char c' = do
    c <- anyChar
    guard (c == c')

我们已经知道char c' = anyChar >>= \c -> guard (c == c') 的定义,所以让我们用它代替:

(>>=)

我们也知道StateT s Maybechar c' = StateT $ \str0 -> case runStateT anyChar str0 of Nothing -> Nothing Just (a, str1) -> runStateT ((\c -> guard (c == c')) a) str1 的倒数,所以:

anyChar

然后我们可以将lambda应用于char c' = StateT $ \str0 -> case runStateT (StateT $ \str -> case str of [] -> Nothing c:cs -> Just (c, cs) ) str0 of Nothing -> Nothing Just (a, str1) -> runStateT ((\c -> guard (c == c')) a) str1

runStateT

现在我们在内部case语句中分发外部case语句:

StateT

...并评估案例陈述:

char c' = StateT $ \str0 -> case (\str -> case str of
        []   -> Nothing
        c:cs -> Just (c, cs) ) str0 of
    Nothing -> Nothing
    Just (a, str1) -> runStateT ((\c -> guard (c == c')) a) str1

然后我们可以将lambda应用于str0

char c' = StateT $ \str0 -> case (case str0 of
        []   -> Nothing
        c:cs -> Just (c, cs) ) of
    Nothing -> Nothing
    Just (a, str1) -> runStateT ((\c -> guard (c == c')) a) str1

为了进一步简化,我们需要了解char c' = StateT $ \str0 -> case str0 of [] -> case Nothing of Nothing -> Nothing Just (a, str1) -> runStateT ((\c -> guard (c == c')) a) str1 c:cs -> case Just (c, cs) of Nothing -> Nothing Just (a, str1) -> runStateT ((\c -> guard (c == c')) a) str1 的作用。以下是它的源代码:

char c' = StateT $ \str0 -> case str0 of
    []   -> Nothing
    c:cs -> runStateT ((\c -> guard (c == c')) c) cs

我们已经知道c的{​​{1}}和char c' = StateT $ \str0 -> case str0 of [] -> Nothing c:cs -> runStateT (guard (c == c')) cs 是什么,所以让我们替换它们:

guard

现在我们可以将其内联到我们的函数中:

guard pred = if pred then return () else mzero

如果我们分发return,我们会得到:

mzero

同样,我们可以将两个分支应用于StateT s Maybe

guard pred = if pred then StateT (\s -> Just ((), s)) else StateT (\_ -> Nothing)

如果我们根本没有使用char c' = StateT $ \str0 -> case str0 of [] -> Nothing c:cs -> runStateT (if (c == c') then StateT (\s -> Just ((), s)) else StateT (\_ -> Nothing) ) cs runStateT个实例,那就是我们手写的等效代码。

最终解析器

我现在将对最后一个函数重复这个过程,但是将推导作为练习留给你:

char c' = StateT $ \str0 -> case str0 of
    []   -> Nothing
    c:cs -> (if (c == c')
        then (\s -> Just ((), s))
        else (\_ -> Nothing) ) cs

...但我们可以进一步简化为:

cs

请注意,与您编写的函数不同,它不使用任何部分函数,​​如char c' = StateT $ \str0 -> case str0 of [] -> Nothing c:cs -> if (c == c') then Just ((), cs) else Nothing 或不完整的模式匹配。此外,您编写的代码无法编译,但即使这样做,它仍然会给出错误的行为。

答案 1 :(得分:4)

您可以使用Control.Monad中名为guard的函数。这有点奇怪的类型:

guard :: MonadPlus m => Bool -> m ()

MonadPlus涵盖所有具有“空”案例的monad。对于列表,这是[];对于Maybe,它是Nothingguard采用布尔值;如果是False,则计算为该空值;否则评估为return ()。此行为在do表示法中非常有用:

x = do guard (not $ null rest' || null rest'' || head rest' /= '|' || head rest'' /= ']')
       Or <$> a <*> b

这里发生的事情很简单。如果条件评估为True,则保护会返回Just (),然后将其忽略,以支持Or <$> a <*> b(因为这是do符号的工作方式)。但是,如果条件为False,则guard会返回Nothingdo会传播Nothing符号的其余部分,以便为您提供where的最终结果:正是你想要的。

为了使代码更具可读性,我还将条件提取到{{1}}块中的自己的变量中。

答案 2 :(得分:0)

根据answer by @TikhonJelvis,我修改了我的整个parse功能。 (OP中的parse'函数位于where的{​​{1}}子句中。)第一个修订使用了符号和`guard

parse

进一步的实验帮助我弄清楚我可以用直接模式匹配替换除parse :: String -> Maybe Wff parse s = do (x, rest) <- parse' s guard $ null rest Just x where parse' ('~':rest) = do guard . not $ null rest (a, rest') <- parse' rest Just (Not a, rest') parse' ('[':rest) = do guard . not $ null rest (a, rest') <- parse' rest guard . not $ null rest' guard $ head rest' == '|' (b, rest'') <- parse' $ tail rest' guard . not $ null rest'' guard $ head rest'' == ']' Just (a `Or` b, tail rest'') parse' (c:rest) = do guard $ isLower c Just (Var c, rest) parse' [] = Nothing 之外的所有用途:

guard