Monadic解析功能珍珠 - 将多个解析器粘合在一起

时间:2014-03-26 21:16:20

标签: parsing haskell

我正在通过功能珍珠纸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是一个消费三个字符的解析器,跳过中间一个,然后返回一对第一个和第二个。我无法弄清楚修改后的输入字符串是如何传递给itemp的第2和第3定义的。我们没有将第一个解析器的结果传递给第二个解析器,依此类推(因为;,使用>>的语法糖,丢弃结果,如类型签名(>>) :: Monad m => m a -> m b -> m b所示。我将非常感谢在item的{​​{1}}的最后两次调用中如何传递修改后的函数。

让我感到困惑的另一件事是pcs的处理 - 它不返回(头部,尾部)对。不应该重新定义如下,因为item解析器根据文章消耗了一个字符:

item

3 个答案:

答案 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没有丢弃其结果(因为>>=分别将它们绑定到ab),而中间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]

在最后一步中,字符串在被多次使用之后被错误地回滚到先前的状态,就像解析器anotherParser2anotherParser3根本没有消耗任何东西一样。通过>>=编写解析器来阻止此错误。

答案 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)