两个班级之前,我们的教授向我们展示了一个Parser模块。
以下是代码:
module Parser (Parser,parser,runParser,satisfy,char,string,many,many1,(+++)) where
import Data.Char
import Control.Monad
import Control.Monad.State
type Parser = StateT String []
runParser :: Parser a -> String -> [(a,String)]
runParser = runStateT
parser :: (String -> [(a,String)]) -> Parser a
parser = StateT
satisfy :: (Char -> Bool) -> Parser Char
satisfy f = parser $ \s -> case s of
[] -> []
a:as -> [(a,as) | f a]
char :: Char -> Parser Char
char = satisfy . (==)
alpha,digit :: Parser Char
alpha = satisfy isAlpha
digit = satisfy isDigit
string :: String -> Parser String
string = mapM char
infixr 5 +++
(+++) :: Parser a -> Parser a -> Parser a
(+++) = mplus
many, many1 :: Parser a -> Parser [a]
many p = return [] +++ many1 p
many1 p = liftM2 (:) p (many p)
今天他给我们做了一个任务,介绍了一个名为(< ++)的左偏置或短路版(+++)。他的暗示是我们考虑(+++)的原始实现。当他第一次向我们介绍+++时,这就是他编写的代码,我将其称为原始实现:
infixr 5 +++
(+++) :: Parser a -> Parser a -> Parser a
p +++ q = Parser $ \s -> runParser p s ++ runParser q s
自从我们介绍解析以来,我遇到了很多麻烦,所以它还在继续。
我已经尝试过/正在考虑两种方法。
1)使用“原始”实现,如p +++ q = Parser $ \ s - > runParser p s ++ runParser q s
2)使用最终实现,如(+++)= mplus
以下是我的问题:
1)如果我使用原始实现,模块将无法编译。错误:不在范围内:数据构造函数'Parser'。它使用(+++)= mplus编译好。使用最终实现避免的原始实现有什么问题?
2)如何检查第一个Parser是否返回任何内容?是不是(isNothing(Parser $ \ s - > runParser p s)在正确的轨道上?看起来应该很容易,但我不知道。
3)一旦我弄清楚如何检查第一个Parser是否返回任何内容,如果我将我的代码基于最终实现,那么它会如此简单吗?:
-- if p returns something then
p <++ q = mplus (Parser $ \s -> runParser p s) mzero
-- else
(<++) = mplus
最佳, 杰夫
P.S。 哦,是的,这个代码做了什么?即使它编译,我也不知道如何测试它以确保它按预期工作。
答案 0 :(得分:4)
这里有很多事情发生了!您正在查看的是(非确定性的)&#34;解析器组合库&#34;您可以在parsec
,attoparsec
,uu-parsinglib
中找到其他示例...这在Haskell中非常常见,但它确实有点复杂。我会在这里解开核心思想。
要考虑的第一个想法是增量解析的概念&#34;步骤&#34;。这是上面代码中由Parser a
表示的内容,您可能会认为它是&#34;运行解析步骤,尝试解析a
&#34;类型的内容。
A&#34;解析步骤&#34;包括查看某种输入的字符流,然后需要很多代表一些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
所做的,我们已经看到的所有三种数据类型都已经Monad
具有这些确切的行为
instance Monad m => Monad (StateT s m) where ...
instance Monad [] where ...
instance Monad Maybe where ...
请注意,StateT
仅在Monad
参数为m
时才是Monad
,这是因为它允许我们将Monad
分层,因此需要调用&#34;内部&#34; StateT String Maybe a
为了做自己的排序。
结果是,通过将这些简单的函数转换为StateT String [] a
或do
,我们立即使用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
实例只是将buck传递给其内部m
,MonadPlus []
。
[]
在做什么?它表示使用&#34;或&#34;组合故障的概念。如果列表Monad
中的mzero
失败,则mplus as bs
会立即失败,as
仅在 bs
和{{}}失败时才会失败{1}}。我们可以把它写成
mplus mzero a = a
mplus a mzero = a
人们可能会认为这是MonadPlus
定义的一种代数法则(尽管这里存在一些争议,但这对代码来说并不重要)。
因此,通过使用mplus
实例来组合解析器,会发生什么?简而言之,它允许你&#34;或&#34;解析器一起使它们只有在所有解析器一起失败时才会失败。
(pa +++ pb +++ pc) is mzero ONLY if pa, pb, AND pc are mzero
这在列表Monad
中很有效,因为它允许我们一起收集多个成功。没有偏见,因为列表monad会尝试所有不同的解析,它们都只是在列表中没有任何优先级。
我们可以将此与Maybe
Monad
进行比较,MonadPlus
Maybe
具有固有的偏见,因为它只会考虑&#34;最好的&#34;在任何给定的时间解析成功。也就是说,我们可以查看instance MonadPlus Maybe where
mzero = Nothing
mplus Nothing x = x
mplus x Nothing = x
mplus (Just a) (Just b) = Just a
mplus
实例
(+++)
在(<++)
定义的最后一行,除了最左边的&#34;成功。这是左偏的核心。
但正如我很久以前所说,对我们来说,平等地优先处理所有解析可能是件坏事。在内存中存储非常痛苦的是存储潜在解析的整个树并在每个新角色被消耗时携带它。
为此,我们可以将StateT sa <++ StateT sb = StateT $ \input -> case sa input of
[] -> sb input
els -> els
偏向sb
。这里的想法是我们想要立即返回成功的解析,并且只在&#34;向右&#34;如果我们必须
sa
如果{{1}}解析器没有产生任何结果,我们仅尝试{{1}}解析器。这意味着我们抛弃了很多潜力&#34;对&#34;在我们的&#34; left&#34;上解析速度。解析。它让我们明智地修剪潜在的解析树。
答案 1 :(得分:2)
Parser
更改为parser
+++
p +++ q = parser $ \s -> runParser p s ++ runParser q s
我们可以稍微扩展一下,使事情更清楚
p +++ q = parser $ \s -> resP ++ resQ
where resP = runParser p s
resQ = runParser q s
这只需要进行少量更改(resP +++ resQ
)即可使<++
偏向偏见。