我正在通过功能珍珠纸Monadic parsing in Haskell
(在haskellforall.com
推荐后阅读该论文以了解解析)。我写了一个实现,直到第3页第4节,如下所示:
newtype Parser a = Parser (String -> [(a,String)])
parse (Parser p) = p
instance Monad Parser where
return a = Parser (\cs -> [(a,cs)])
p >>= f = Parser (\cs -> concat [parse (f a) cs' | (a,cs') <- parse p cs])
item :: Parser Char
item = Parser (\cs -> case cs of
"" -> []
(c:cs) -> [(c,cs)])
p :: Parser (Char,Char)
p = do { a <- item; item; b <- item; return (a,b)}
根据论文,p
是一个消费三个字符的解析器,跳过中间一个,然后返回一对第一个和第二个。我无法弄清楚修改后的输入字符串是如何传递给item
中p
的第2和第3定义的。我们没有将第一个解析器的结果传递给第二个解析器,依此类推(因为;
,使用>>
的语法糖,丢弃结果,如类型签名(>>) :: Monad m => m a -> m b -> m b
所示。我将非常感谢在item
的{{1}}的最后两次调用中如何传递修改后的函数。
让我感到困惑的另一件事是p
中cs
的处理 - 它不返回(头部,尾部)对。不应该重新定义如下,因为item
解析器根据文章消耗了一个字符:
item
答案 0 :(得分:4)
语法;
并不总是>>
的语法糖。
相反,我们有:
do m ; n = m >> n
do x<-m ; n = m >>= \x -> n
(以上翻译已简化,the full gory details可在Haskell报告中找到)
因此,您对p
的定义等同于:
p = item >>= \a -> ( item >> (item >>= \b -> return (a,b) ))
在这里,您可以看到第一个和第三个item
没有丢弃其结果(因为>>=
分别将它们绑定到a
和b
),而中间item
确实如此。
还要注意代码
\cs -> case cs of
"" -> []
(c:cs) -> [(c,cs)]
具有误导性,因为它定义变量cs
两次:一次在\cs
,一次在
模式(c:cs)
。它相当于
\cs -> case cs of
"" -> []
(x:xs) -> [(x,xs)]
这澄清了最终String
输出不原始cs
,而是尾xs
。
在评论中,海报想知道为什么item
的三种用法不会返回相同的结果,即为什么return (a,b)
中的字符a
不等于{{1} }}。这是由b
monadic运算符引起的,该运算符在>>=
monad中自动将每个Parser
出现的输出字符串xs
提供给下一个item
monadic运算符。实际上,这个monad的重点是帮助将每个解析器的“剩余”输出作为下一个“待消耗”输入。这有两个好处:它使程序员不必编写代码来传递这个字符串,并确保字符串不会意外地“重绕”到先前的状态。为了说明后一点,这里有一些错误的代码:
let [(c1,s1)] = someParser someInitialString
[(c2,s2)] = anotherParser1 s1
[(c3,s3)] = anotherParser2 s2
[(c4,s4)] = anotherParser3 s3
[(c5,s5)] = anotherParser4 s2 -- Whoops! Should have been s4
in [c1,c2,c3,c4,c5]
在最后一步中,字符串在被多次使用之后被错误地回滚到先前的状态,就像解析器anotherParser2
和anotherParser3
根本没有消耗任何东西一样。通过>>=
编写解析器来阻止此错误。
答案 1 :(得分:3)
我会尝试更多地了解>>
。
正如你在另一个答案中看到的那样,你应该将这些做法变成>>=
,以便更好地了解正在发生的事情。
让我们编写一个解析器来解析两个字符并返回它们。
twoChars :: Parser (Char,Char)
twoChars = do
i <- item
j <- item
return (i,j)
现在,desugar do
语法:
twoChars :: Parser (Char,Char)
twoChars =
item >>= (\i ->
item >>= (\j ->
return (i,j) ) )
为了清楚起见,我放了括号。如您所见,第二个item
接收匿名函数中第一个item
解析器的结果,结果绑定到i
。 >>=
函数接受解析器,函数并返回解析器。了解它的最佳方法是将其插入定义:
f = \i → item »= \j → return (i,j)
twoChars = item >>= f
twoChars = Parser (\cs -> concat [parse (f a) cs' | (a,cs') <- parse item cs])
所以我们找回了一个新的Parser。试着想象它会对输入做什么&#34; abc&#34;。 cs
绑定到&#34; abc&#34;,项目解析器用于返回[(&#39; a&#39;&#39;&#34;&#34; bc&#34;)]。现在,我们将f
应用于&#39; a&#39;,以取回新的解析器:
item >>= \j -> return ('a',j)
此解析器将传递剩余的字符串以进行处理("bc"
),当item
时,它将使用b
解析器获取\j
}以上绑定到b
。然后,我们得到return ('a','b')
语句,将('a','b')
放入只返回('a','b')
的解析器中。
我希望这可以清除信息流的发生方式。现在,假设你想忽略一个角色。你可以这样做。
twoChars :: Parser (Char,Char)
twoChars =
item >>= \i ->
item >>= \j ->
item >>= \k ->
return (i,k)
对于示例&#34; abc&#34;,j
绑定到'b'
,你永远不会使用它。我们可以j
替换_
。
twoChars :: Parser (Char,Char)
twoChars =
item >>= \i ->
item >>= \_ ->
item >>= \k ->
return (i,k)
但我们也知道>> :: m a -> m b -> m b
可以定义为:
p >> q = p >>= \_ -> q
所以我们留下了
twoChars :: Parser (Char,Char)
twoChars =
item >>= \i ->
item >>
item >>= \k ->
return (i,k)
最后,你可以将其加回do
。 >>
的应用只是在没有边界的单行语句中糖。结果是:
twoChars :: Parser (Char,Char)
twoChars = do
i <- item
item
j <- item
return (i,j)
希望这能解决一些问题。
答案 2 :(得分:1)
的翻译更统一
p3 = do { a <- item; item; b <- item; return (a,b)}
-- do { a <- item; z <- item; b <- item; return (a,b)} -- z is ignored
是
p3 = item >>= (\a ->
item >>= (\z ->
item >>= (\b ->
return (a,b)))) -- z is unused
(这里的关键观察是函数是嵌套的)。这意味着
-- parse (return a) cs = [(a,cs)]
-- parse (p >>= f) cs = [r | (a,cs1) <- parse p cs, -- concat
-- r <- parse (f a) cs1] ) -- inlined !
parse p3 cs
= [ r | (a,cs1) <- parse item cs,
r <- [ r | (z,cs2) <- parse item cs1,
r <- [ r | (b,cs3) <- parse item cs2,
r <- -- parse (return (a,b)) cs3
[((a,b),cs3)]]]] -- z is unused
= [ ((a,b),cs3) | (a,cs1) <- parse item cs,
(_,cs2) <- parse item cs1,
(b,cs3) <- parse item cs2]
所以你看,"the input string"确实发生了变化:首先是cs
,然后是cs1
,然后是cs2
。
这是所有Parser
标记和do
语法背后的简单实际计算。这完全是关于嵌套循环中输入和输出的链接,最后:
parse p3 cs =
for each (a,cs1) in (parse item cs):
for each (z,cs2) in (parse item cs1):
for each (b,cs3) in (parse item cs2):
yield ((a,b),cs3)