【问题标题】:Error detection and reporting using Maybe使用 Maybe 进行错误检测和报告
【发布时间】:2013-06-12 00:47:12
【问题描述】:

我正在用 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,我在最后一行使用应用样式来传播Nothing 结果。这让我可以从if 条件中取出a == Nothingb == Nothing。如何使用MaybeApplicativeMonad 实例来折叠此if 的其余部分?

【问题讨论】:

    标签: parsing haskell text-parsing


    【解决方案1】:

    我实际上会倒着做这个问题:我将从一元解决方案开始,然后从它倒退到手动解决方案。如果您手动找到正确的解决方案,这将产生相同的代码。

    一元解析器的典型类型签名是这样的:

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

    请注意与您的表单的细微差别,您在Maybe 的外部有最后的String。两者都有效,但我更喜欢这种形式,因为如果解析失败,我认为剩余的 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

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

    import Control.Monad
    
    char :: Char -> Parser ()
    char c' = do
        c <- anyChar
        guard (c == c')
    

    这很容易! guard 需要我们的 monad 的 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',"")
    

    向后工作

    这将我们带到您最初的问题:您如何手写相同的行为?我们将从MonadMonadPlus 实例向后推导它们在幕后为我们做了什么。

    为此,我们必须推断StateTMonadMonadPlus 实例在其基本单子为Maybe 时会减少什么。让我们从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
    

    请注意,它是在基本 monad 的一般术语中定义的。这意味着我们还需要 MaybeMonad 实例来派生上述代码的作用:

    instance Monad Maybe where
        return  = Just
    
        m >>= f = case m of
            Nothing -> Nothing
            Just a  -> f a
    

    如果我们将 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
    

    我们可以做同样的事情来为StateT s Maybe 派生Monad 实例。我们只需要为StateT 和`Maybe:

    获取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
    

    ...并将它们合二为一:

    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')
    

    我们刚刚导出了 (&gt;&gt;=)StateT s Maybe 所做的事情,所以让我们将其替换为:

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

    我们已经知道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
    

    我们也知道runStateTStateT的倒数,所以:

    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
    

    现在我们将外部 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
    

    ...并评估案例陈述:

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

    然后我们可以将 lambda 应用到c

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

    为了进一步简化,我们需要知道guard 做了什么。这是它的源代码:

    guard pred = if pred then return () else mzero
    

    我们已经知道StateT s Maybereturnmzero 是什么,所以让我们将它们替换为:

    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
    

    如果我们根本不使用 MonadMonadPlus 实例,这就是我们手动编写的等效代码。

    最终解析器

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

    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
    

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

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

    请注意,与您编写的函数不同,它不使用任何部分函数,​​例如 tail 或不完整的模式匹配。此外,您编写的代码无法编译,但即使编译了,它仍然会给出错误的行为。

    【讨论】:

    • "您编写的代码无法编译,但即使编译了,它仍然会给出错误的行为。"除了缺少Wff 的定义之外,我给出的代码中没有编译什么?您能否提供一个会导致错误行为的示例输入?
    • @Code-Guru 我发现了这个问题。这是因为您没有解析基本命题的基本案例(即AB),所以我错误地认为您将Chars 存储在Wff 类型中。
    • 使用Char 是对当前讨论的合理简化。对于过度简化我最初发布的代码,我深表歉意。我在我的 github repo 中添加了对应版本的链接。
    【解决方案2】:

    您可以使用来自Control.Monad 的函数guard。这有一个有点奇怪的类型:

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

    MonadPlus 涵盖了所有具有“空”大小写的单子。对于列表,这是[];对于Maybe,它是Nothingguard 采用布尔值;如果是False,则计算为这个空值;否则它的计算结果为return ()。这种行为在do 表示法中最有用:

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

    这里发生的事情很简单。如果条件计算为True,则guard 返回Just (),然后将其忽略以支持Or &lt;$&gt; a &lt;*&gt; b(因为do 表示法就是这样工作的)。但是,如果条件是 Falseguard 会返回 Nothing,它会通过 do 表示法的其余部分传播,从而为您提供 Nothing 的最终结果:正是您想要的。

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

    【讨论】:

    • 感谢您提供的信息。我会进一步研究guard。一个问题/评论:我看到你用not 否定了我原来的条件。我认为这是因为您正在颠倒我的if 表达式的逻辑,对吗?这是有道理的……我认为……我需要再考虑一下。
    • 是的。 guard True 不做任何事情,而 guard False 导致 Nothing(或任何你的单子“零”值)。
    • 谢谢。如果您有时间,请查看我的下一个问题,您的回答引发了该问题:stackoverflow.com/questions/17056881/monoid-vs-monadplus
    【解决方案3】:

    基于answer by @TikhonJelvis,我改进了我的整个parse 函数。 (来自 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
    

    一些进一步的实验帮助我发现我可以用直接模式匹配替换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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-05-22
      • 2016-11-11
      • 2014-09-17
      • 2011-04-12
      • 1970-01-01
      • 2012-08-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多