简体   繁体   English

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

[英]Error detection and reporting using Maybe

I am writing a propositional logic parser in Haskell. 我正在Haskell中编写一个命题逻辑解析器。 I am doing the parsing by hand for now as a learning exercise. 我现在正在手工解析作为学习练习。 Eventually I will tackle Parsec. 最终我将解决Parsec。 In the mean time, I am trying to wrap my head around Monads. 与此同时,我正试图绕着Monads。 In particular, I am using Maybe to report errors from my parse function. 特别是,我使用Maybe来报告我的parse函数中的错误。 My current trouble is with part of a helper function: 我目前的麻烦是辅助函数的一部分:

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

(For reference, the full parse function can be found here .) (作为参考,可以在此处找到完整的parse功能。)

This code parses a proposition of the form [ A | B ] 这段代码解析了[ A | B ]形式的命题 [ A | B ] where A and B are any arbitrary propositions. [ A | B ]其中AB是任意命题。 As you can see, I am using applicative style on the last line to propagate the Nothing result if a previous recursive call results in a Nothing . 正如您所看到的,如果先前的递归调用导致Nothing ,我在最后一行使用applicative样式来传播Nothing结果。 This allowed me to take out a == Nothing and b == Nothing from the if condition. 这允许我从if条件中取出a == Nothingb == Nothing How can I use the Applicative or Monad instance of Maybe to colapse the rest of this if ? 我如何使用ApplicativeMonad的实例Maybe到COLAPSE这剩下的if

I will actually do the problem backwards: I will begin from the monadic solution and work backwards from it to the hand-rolled solution. 我实际上会向后解决这个问题:我将从monadic解决方案开始,然后从它向后工作到手动解决方案。 This will produce the same code that you would get if you arrived at the correct solution by hand. 如果您手动找到正确的解决方案,这将生成相同的代码。

The typical type signature of a monadic parser is of the form: monadic解析器的典型类型签名具有以下形式:

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

Notice the slight difference with your form, where you have the final String on the outside of the Maybe . 注意与表单的细微差别,你在Maybe的外面有最终的String Both are valid, but I prefer this form more because I consider the leftovers String invalid if the parse failed. 两者都有效,但我更喜欢这种形式,因为如果解析失败,我会认为剩余的String无效。

This type is actually a special case of StateT , which is defined as: 这种类型实际上是StateT一个特例,它被定义为:

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

Notice that if we choose: 请注意,如果我们选择:

s = String
m = Maybe

... we get back the Parser type: ...我们回到Parser类型:

type Parser a = StateT String Maybe a

-- or: type Parser = StateT String Maybe

What's cool about this is that we only need to define one parser by hand, which is the parser that retrieves a single character: 这很酷的是我们只需要手动定义一个解析器,它是检索单个字符的解析器:

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

Notice that if we removed the StateT wrapper, the type of anyChar would be: 请注意,如果我们删除了StateT包装的类型, anyChar将是:

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

When we wrap it in StateT it becomes: 当我们将它包装在StateT它变为:

anyChar :: StateT String Maybe Char

... which is just Parser Char . ......这只是Parser Char

Once we have that primitive parser, we can define all the other parsers using StateT 's Monad interface. 一旦我们有了这个原始解析器,我们就可以使用StateTMonad接口定义所有其他解析器。 For example, let's define a parser that matches a single character: 例如,让我们定义一个匹配单个字符的解析器:

import Control.Monad

char :: Char -> Parser ()
char c' = do
    c <- anyChar
    guard (c == c')

That was easy! 那很简单! guard requires a MonadPlus instance for our monad, but we already have one. guard需要一个MonadPlus实例用于我们的monad,但我们已经有了一个。 The reason why is thanks to the following two MonadPlus instances: 原因是由于以下两个MonadPlus实例:

instance (MonadPlus m) => MonadPlus (StateT s m) where ...

instance MonadPlus Maybe where ...

The combination of these two instances means that StateT s Maybe automatically implements MonadPlus , so we can use guard and it will just magically do "the right thing". 这两个实例的组合意味着StateT s Maybe自动实现MonadPlus ,因此我们可以使用guard ,它只会神奇地做“正确的事情”。

With those two parser in hand, your final parser becomes very easy to write: 有了这两个解析器,你的最终解析器变得非常容易编写:

data Wff = Or Char Char deriving (Show)

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

That's much clearer and easier to understand what is going on. 这更加清晰,更容易理解发生了什么。 It also works: 它也有效:

>>> runStateT parseWff "[A|B]"
Just (Or 'A' 'B',"")

Working Backwards 向后工作

This brings us to your original question: How do you hand-write the same behavior? 这将我们带到您原来的问题:您如何手写相同的行为? We will work backwards from the Monad and MonadPlus instances to deduce what they are doing under the hood for us. 我们将从MonadMonadPlus实例向后工作,为我们演绎他们在MonadPlus做的事情。

To do this, we must deduce what the Monad and MonadPlus instances for StateT reduce to when its base monad is Maybe . 要做到这一点,我们必须推断出的MonadMonadPlus的实例StateT减少时,其基础是单子Maybe Let's begin from the Monad instance for StateT : 让我们从StateTMonad实例开始:

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

Notice that it is defined in generically terms of the base monad. 请注意,它是以基本monad的一般术语定义的。 This means we also need the Monad instance for Maybe to derive what the above code does: 这意味着我们还需要MaybeMonad实例来推导上面的代码:

instance Monad Maybe where
    return  = Just

    m >>= f = case m of
        Nothing -> Nothing
        Just a  -> f a

If we substitute the Maybe monad instance into the StateT monad instance we get: 如果我们将Maybe monad实例替换为StateT monad实例,我们得到:

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

We can do the same thing to derive the Monad instance for StateT s Maybe . 我们可以为StateT s Maybe派生Monad实例做同样的事情。 We just have to take the MonadPlus instances for StateT and `Maybe: 我们只需要为StateT和“可能”采用MonadPlus实例:

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

... and combine them into one: ......并将它们组合成一个:

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

Now we can derive what our parsers are doing under the hood. 现在我们可以得出我们的解析器在幕后做的事情。 Let's begin with the char parser: 让我们从char解析器开始:

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

This desugars to: 这令人厌恶:

char c' = anyChar >>= \c -> guard (c == c')

We just derived what (>>=) does for StateT s Maybe , so lets substitute that in: 我们只是派生了什么(>>=)StateT s Maybe做了StateT s Maybe ,所以让我们替换它:

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

We already know the definition of anyChar , so let's substitute that in: 我们已经知道了anyChar的定义,所以让我们用它代替:

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

We also know that runStateT is the inverse of StateT , so: 我们也知道, runStateT是逆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

We can then apply the lambda to str0 : 然后我们可以将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

Now we distribute the outer case statement over the inner case statement: 现在我们在内部case语句上分发外部case语句:

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

... and evaluate the case statements: ......并评估案例陈述:

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

Then we can apply the lambda to c : 然后我们可以将lambda应用于c

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

To simplify that further, we need to know what guard does. 为了进一步简化,我们需要知道guard作用。 Here is the source code for it: 这是它的源代码:

guard pred = if pred then return () else mzero

We already know what return and mzero for StateT s Maybe are, so let's substitute them in: 我们已经知道StateT s Maybe returnmzero是什么,所以让我们替换它们:

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

Now we can inline that into our function: 现在我们可以将其内联到我们的函数中:

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

If we distribute runStateT over that we get: 如果我们分配runStateT ,我们得到:

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

Similarly, we can apply both branches to cs : 同样,我们可以将两个分支应用于cs

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

That's the equivalent code we would have written by hand had we not used the Monad or MonadPlus instances at all. 如果我们根本没有使用MonadMonadPlus实例,那就是我们手写的等效代码。

The Final Parser 最终的解析器

I will now repeat this process for the last function, but leave the derivation as an exercise for you: 我现在将为最后一个函数重复此过程,但将派生作为练习:

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

parseWff = StateT $ \str0 -> case str0 of
    []     -> Nothing
    c1:str1 -> if (c1 == '[')
        then case str1 of
            []      -> Nothing
            c2:str2 -> case str2 of
                []      -> Nothing
                c3:str3 -> if (c3 == '|')
                    then case str3 of
                        []      -> Nothing
                        c4:str4 -> case str4 of
                            []      -> Nothing
                            c5:str5 -> if (c5 == ']')
                                then Just (Or c2 c4, str5)
                                else Nothing
                    else Nothing
        else Nothing

... but we can further simplify that to: ......但我们可以进一步简化为:

parseWff = StateT $ \str0 -> case str0 of
    '[':c2:'|':c4:']':str5 -> Just (Or c2 c4, str5)
    _                      -> Nothing

Notice that, unlike the function you wrote, this doesn't use any partial functions like tail or incomplete pattern matches. 请注意,与您编写的函数不同,它不使用任何部分函数,​​如tail或不完整模式匹配。 Also, the code you wrote doesn't compile, but even if it did, it would still give the wrong behavior. 此外,您编写的代码不会编译,但即使它编译,它仍然会给出错误的行为。

You can use a function from Control.Monad called guard . 您可以使用Control.Monad名为guard的函数。 This has a slightly odd type: 这有点奇怪的类型:

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

MonadPlus covers all monads that have some "empty" case. MonadPlus涵盖所有具有“空”案例的monad。 For lists, this is [] ; 对于列表,这是[] ; for Maybe it is Nothing . 因为它Maybe Nothing guard takes a boolean; guard需要一个布尔值; if it is False , it evaluates to this empty value; 如果为False ,则计算为该空值; otherwise it evaluates to return () . 否则它评估为return () This behavior is mostly useful in do notation: 此行为是最有用do记号:

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

What happens here is simple. 这里发生的事情很简单。 If the condition evaluates to True , guard returns Just () , which is then ignored in favor of Or <$> a <*> b (since that's how do notation works). 如果条件评估为True ,门卫返回Just ()然后将其赞成忽略Or <$> a <*> b (因为这是如何do的符号的作品)。 However, if the condition is False , guard returns Nothing , which propagates through the rest of the do notation to give you an end result of Nothing : exactly what you wanted. 然而,如果条件为Falseguard返回Nothing ,其传播通过其余do记号给你的最终结果Nothing :你想要什么。

To make the code more readable, I would also pull the condition out into its own variable in a where block. 为了使代码更具可读性,我还将条件拉出到where块中的自己的变量中。

Based on the answer by @TikhonJelvis , I revamped my whole parse function. 根据@TikhonJelvis回答 ,我修改了我的整个parse功能。 (The parse' function from the OP is in the where clause of parse .) The first revision uses do notation and `guard (来自OP的parse'函数位于parsewhere子句中。)第一个修订版使用do符号和`guard

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

Some further experimentation helped me figure out that I can replace all but one of the uses of guard with direct pattern matching: 一些进一步的实验帮助我弄清楚我可以用直接模式匹配替换除guard所有用途:

parse :: String -> Maybe Wff
parse s = do
  (x, "") <- parse' s
  Just x
    where  parse' ('~':rest) = do
             (a, rest') <- parse' rest
             Just (Not a, rest')
           parse' ('[':rest) = do
             (a, ('|':rest')) <- parse' rest
             (b, (']':rest'')) <- parse' rest'
             Just (a `Or` b, rest'')
           parse' (c:rest) = do
             guard $ isLower c
             Just (Var c, rest)
           parse' [] = Nothing

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM