我正在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 ]
形式的命题,其中A
和B
是任意命题。如您所见,如果先前的递归调用导致Nothing
,我在最后一行使用applicative样式来传播Nothing
结果。这样我就可以从a == Nothing
条件中取出b == Nothing
和if
。如何使用Applicative
或Monad
Maybe
实例来完成其余if
?
答案 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 Maybe
是char 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
,它是Nothing
。 guard
采用布尔值;如果是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
会返回Nothing
,do
会传播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