【问题标题】:Unable to understand a mutual recursion无法理解相互递归
【发布时间】:2011-05-21 09:58:41
【问题描述】:

我正在阅读《Haskell 编程》,第 8 章,作者给出了一个编写解析器的例子。 完整来源在这里:http://www.cs.nott.ac.uk/~gmh/Parsing.lhs 我无法理解以下部分:many 允许p 的零个或多个应用程序, 而many1 至少需要一个成功的申请:

many        ::    Parser a → Parser [a ]
many p      =     many1 p +++ return [ ]
many1       ::    Parser a → Parser [a ]
many1 p     = do v ← p
                 vs ← many p
                 return (v : vs)

递归调用如何发生在

vs <- many p

vsmany p的结果值,但是很多p叫做many1 p,所有many1在其定义中都是do符号,又是结果值vvs,递归调用什么时候返回? 为什么下面的sn-p可以返回[("123","abc")]

> parse (many digit) "123abc"
[("123", "abc")]

【问题讨论】:

    标签: haskell recursion mutual-recursion


    【解决方案1】:

    递归在v &lt;- p 行停止。当无法再解析 p 时,解析器的一元行为只会将 [] 传播到计算结束。

    p >>= f =  P (\inp -> case parse p inp of
                            []        -> [] -- this line here does not call f
                            [(v,out)] -> parse (f v) out)
    

    第二个函数是用 do-notation 编写的,这只是下面的一个很好的语法:

    many1 p = p >>= (\v -> many p >>= (\vs -> return (v : vs)))
    

    如果解析 p 产生一个空列表 [],则不会调用函数 \v -&gt; many p &gt;&gt;= (\vs -&gt; return (v : vs)),从而停止递归。

    【讨论】:

    • 对不起,我刚看了这本书的第8章,还不知道什么是monadic行为,你能解释一下更容易理解吗?
    • @Sawyer:我努力解释正在发生的事情,但没有深入研究“monadic”是什么,这对许多人来说似乎是一个令人困惑的话题,我相信你的书会做一个到时候更好地解释它。如果它不转,请收藏this link
    • 简而言之,停止递归的模式匹配在第 78 页定义的“选择”函数+++ 中。正如many 调用+++ 一样,递归停止的地方。
    • 这是一个老问题,但我发现自己陷入了同样的问题。理解 many 和 many1... 我想我会尝试使用像“a”这样的小输入来跟踪程序,并找出在调用 many 时会在哪部分产生空字符串以及为什么在调用 many1 时不会产生空字符串?
    【解决方案2】:

    最后一个问题:

    > parse (many digit) "123abc"
    [("123", "abc")]
    

    表示解析成功,因为答案列表中至少返回了一个结果。 Hutton 解析器总是返回一个列表——空列表意味着解析失败。

    结果 ("123", "abc") 表示解析找到了三个数字 "123" 并停在不是数字的 'a' - 所以“输入的其余部分”是“abc”。

    请注意,many 的意思是“尽可能多”,而不是“一个或多个”。如果它是“一个或多个”,你会得到这个结果:

    [("1", "23abc"), ("12", "3abc"), ("123", "abc")]
    

    这种行为对于确定性解析来说不是很好,尽管有时自然语言解析可能需要它。

    【讨论】:

    • 结果是[("123","abc")],从return语句来看,我猜最后v是"123",vs是"abc",这样v:vs 可以产生 [("123","abc")],但是对 v 或 vs 没有任何操作,“123”和“abc”从何而来?
    • many1 定义中的 return 语句操纵 vvs - 将它们组合成一个列表 (v : vs) - 你应该真正编辑你的问题,所以 return 语句是在 do-block 内对齐。由于输入中有三位数字并且many 提供了空列表,因此您会得到('1': ('2': ('3' : []))),即字符串“123”。 "abc" 是未解析的其余输入。
    • 还要注意,第 76 页上的 item 解析器是实际分离输入字符串的函数。第 79 页上的函数 digit 是使用第 78 页上的函数 sat(满足)构建的,该函数实际上调用了 item。这是编程的“组合器”风格,其中函数是用将其他函数作为参数的函数构建的。诚然,在您获得相当多的经验之前,很难跟踪这种风格的程序的控制流。
    【解决方案3】:

    让我把它剥离到最简单的部分,以明确为什么do-blocks 被简单地理解为命令式代码会被误解。考虑一下这个 sn-p:

    doStuff :: Maybe Int
    doStuff = do
        a <- Nothing
        doStuff
    

    看起来doStuff 将永远递归,毕竟它被定义为执行一系列以doStuff 结尾的事情。但是do-block 中的行序列不仅仅是按顺序执行的操作序列。如果您位于do-block 中的某个点,则处理该块其余部分的方式由&gt;&gt;= 的定义决定。在我的示例中,&gt;&gt;= 的第二个参数仅在第一个参数不是 Nothing 时使用。所以递归永远不会发生。

    类似的事情可能发生在许多不同的 monad 中。您的示例稍微复杂一点:当没有更多方法可以解析某些内容时,&gt;&gt;= 之后的内容将被忽略。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多