这里发生了很多事情!您正在查看的是一个(非确定性的)“解析器组合库”,您可以在parsec
, attoparsec
, uu-parsinglib
... 中找到其他示例,这在 Haskell 中是一个很常见的想法,但它肯定有点复杂。我将在这里解开核心思想。
要考虑的第一个想法是增量解析“步骤”的概念。这就是上面代码中表示的Parser a
内容,您可能会将其视为“运行解析步骤以尝试解析某种类型的内容a
”。
“解析步骤”涉及查看某种类型的字符输入流,提取但需要许多字符来表示某种a
类型,然后返回未使用的新鲜a
字符和剩余字符。在这个级别的描述中,很容易用 Haskell 写出来
String {- input stream -} -> (a {- fresh -}, String {- leftovers -})
这是解析器步骤的基础,值得注意的是,在解析库之外我们称之为State String a
.
newtype State s a = State { runState :: s -> (a, s) }
>>> :t runState (undefined :: State String a)
String -> (a, String)
我们也可以尝试以这种分解的格式构建解析器步骤。考虑一个解析器,它消耗单个字符来创建一个Int
parseInt :: String -> (Int, String)
parseInt (x:xs) = case x of
'0' -> (0, xs)
'1' -> (1, xs)
...
'9' -> (9, xs)
_ -> error "What! Failure!"
parseInt [] = error "What! Another failure!"
>>> parseInt "3leftovers"
(3, "leftovers")
马上我们可以看到这个模型太简单了——我们只能通过一直抛出错误到运行时来提供解析器失败。这很危险,表明我们对解析器的建模很差。不过,我们可以非常简单地为其添加失败。
-- String -> Maybe (a, String)
parseInt :: String -> Maybe (Int, String)
parseInt [] = Nothing
parseInt (x:xs) = case x of
'0' -> Just (0, xs)
'1' -> Just (1, xs)
...
'9' -> Just (9, xs)
_ -> Nothing
>>> parseInt "foo"
Nothing
这也是一个非常常见的 Haskell 主题,即使在称为 State Transformer 或StateT
. 定义看起来像这样
newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }
>>> :t runStateT (undefined :: StateT String Maybe a)
String -> Maybe (a, String)
它允许我们将Maybe
体现的失败概念与State
原始解析器的概念结合起来。事实上,这就是你的教授用他自己的版本所做的,除了他没有使用Maybe
他使用的[]
>>> :t runStateT (undefined :: StateT String [] a)
String -> [(a, String)]
它允许失败(作为空列表[]
)和多个同时成功。这就是使他的解析器具有不确定性的原因——它在每个解析步骤中收集并处理多个成功。这可能对记忆非常不利,但却是一种谨慎使用的强大技术。
不过,到目前为止,我所写的内容中还缺少其他内容——我们如何将多个解析器组合在一起?parseInt
比如跑三遍就相当痛苦
parse3Ints :: String -> Maybe ((Int, Int, Int), String)
parse3Ints input = case parseInt input of
Nothing -> Nothing
Just (i1, input') -> case parseInt input' of
Nothing -> Nothing
Just (i2, input'') -> case parseInt input'' of
Nothing -> Nothing
Just (i3, leftovers) -> Just ((i1, i2, i3), leftovers)
啊。我们能做得更好吗?我们需要以某种方式将失败和input
字符串的传递联系在一起。幸运的是,这正是Monad
s 所做的,我们所看到的所有三种数据类型都Monad
具有这些确切的行为
instance Monad m => Monad (StateT s m) where ...
instance Monad [] where ...
instance Monad Maybe where ...
请注意,仅当其参数为时才StateT
为 a ---这是因为它允许我们将s 分层,因此它需要调用“内部”以进行自己的排序。Monad
m
Monad
Monad
结果是,通过将这些简单的函数转换为StateT String Maybe a
orStateT String [] a
我们立即开始使用do
-notation 让内置Monad
实例处理我们的复杂排序
parse3Ints :: StateT String Maybe (Int, Int, Int)
parse3Ints = do
i1 <- parseInt
i2 <- parseInt
i3 <- parseInt
return (i1, i2, i3)
-- or even
parse3Ints = liftM3 (,,) parseInt parseInt parseInt
这里的最后一个兴趣点是关于你的教授的问题(+++)
。在这里,他使用了mplus
来自其类型类的函数,MonadPlus
并且StateT
是[]
实例。我们可以看看那个代码
instance MonadPlus [] where
mzero = []
mplus as bs = as ++ bs
instance MonadPlus m => MonadPlus (StateT s m) where
mzero = StateT $ \input -> mzero
mplus (StateT sa) (StateT sb) = StateT $ \input -> mplus (sa input) (sb input)
所以我们可以看到,这段代码的真正权重在于[]
实例,因为StateT
实例只是将责任推给了它的内部Monad
, m
。
在MonadPlus []
做什么?它表示使用“或”组合失败的概念。如果[]
是列表中的失败,Monad
那么mzero
是立即失败,并且仅mplus as bs
当两者 都是失败时才失败。我们可以写成as
bs
mplus mzero a = a
mplus a mzero = a
人们可能会认为这是一种代数定律的定义MonadPlus
(尽管这里有一些争议,但这对这里的代码无关紧要)。
那么通过使用mplus
实例来组合解析器,是怎么回事呢?简而言之,它允许您将解析器“或”在一起,这样它们只有在所有解析器一起失败时才会失败。
(pa +++ pb +++ pc) is mzero ONLY if pa, pb, AND pc are mzero
这在列表中效果很好,Monad
因为它允许我们一起收集多个成功。没有偏见,因为 list monad 尝试了所有不同的解析,它们都只是进入列表而没有任何优先级。
我们可以将其与Maybe
Monad
固有的偏见进行比较,因为它只能在任何给定时间考虑“最佳”解析成功。也就是说,我们可以看一下MonadPlus
实例Maybe
instance MonadPlus Maybe where
mzero = Nothing
mplus Nothing x = x
mplus x Nothing = x
mplus (Just a) (Just b) = Just a
在mplus
定义的最后一行,我们抛弃了除了“最左边”的成功之外的所有东西。这是左倾的核心。
但正如我很久以前所说的,对我们来说,平等地优先考虑所有解析可能是一件坏事。存储整个潜在解析树并在每个新字符被消耗时随身携带它在内存中可能非常痛苦。
为此,我们可以将偏差保留(+++)
为(<++)
。这里的想法是我们希望立即返回成功的解析,并且只有在必须时才“向右”传递
StateT sa <++ StateT sb = StateT $ \input -> case sa input of
[] -> sb input
els -> els
这里我们只在解析器没有结果的情况下尝试sb
解析器。sa
这意味着我们丢弃了许多潜在的“右”解析来加快我们“左”解析的速度。它让我们可以明智地修剪潜在解析树。