2010-06-29 56 views
5

一些背景优先。我目前正在学习一些关于monadic解析器组合器的东西。当我试图从this paper(第16-17)转移“chainl1”功能,我想出了这个解决方案:计算表达式中的递归函数

let chainl1 p op = parser { 
    let! x = p 
    let rec chainl1' (acc : 'a) : Parser<'a> = 
     let p' = parser { 
      let! f = op 
      let! y = p 
      return! chainl1' (f acc y) 
      } 
     p' <|> succeed acc 
    return! chainl1' x 
} 

我测试了一些大的输入功能,并得到了StackOverflowException。现在我想知道,是否有可能重写递归函数,它使用一些计算表达式,所以它使用尾递归?

当我扩大计算表达式时,我看不出它通常是可能的。

let chainl1 p op = 
    let b = parser 
    b.Bind(p, (fun x -> 
    let rec chainl1' (acc : 'a) : Parser<'a> = 
     let p' = 
      let b = parser 
      b.Bind(op, (fun f -> 
      b.Bind(p, (fun y -> 
      b.ReturnFrom(chainl1' (f acc y)))))) 
     p' <|> succeed acc 
    b.ReturnFrom(chainl1' x))) 

回答

6

在代码中,下面的函数不是尾递归,因为 - 在每次迭代 - 这使得无论是p'succeed之间做出选择:

// Renamed a few symbols to avoid breaking SO code formatter 
let rec chainl1Util (acc : 'a) : Parser<'a> = 
    let pOp = parser { 
    let! f = op 
    let! y = p 
    return! chainl1Util (f acc y) } 
    // This is done 'after' the call using 'return!', which means 
    // that the 'cahinl1Util' function isn't really tail-recursive! 
    pOp <|> succeed acc 

根据您的实现解析器组合中,以下重写可以工作(我不是专家,但它可能是值得尝试此):

let rec chainl1Util (acc : 'a) : Parser<'a> = 
    // Succeeds always returning the accumulated value (?) 
    let pSuc = parser { 
    let! r = succeed acc 
    return Choice1Of2(r) } 
    // Parses the next operator (if it is available) 
    let pOp = parser { 
    let! f = op 
    return Choice2Of2(f) } 

    // The main parsing code is tail-recursive now... 
    parser { 
    // We can continue using one of the previous two options 
    let! cont = pOp <|> pSuc 
    match cont with 
    // In case of 'succeed acc', we call this branch and finish... 
    | Choice1Of2(r) -> return r 
    // In case of 'op', we need to read the next sub-expression.. 
    | Choice2Of2(f) -> 
     let! y = p 
     // ..and then continue (this is tail-call now, because there are 
     // no operations left - e.g. this isn't used as a parameter to <|>) 
     return! chainl1Util (f acc y) } 

一般情况下,模式编写尾递归函数中的计算式的作品。这样的事情就可以了(对于在一个方式,让尾递归实现的计算式):

let rec foo(arg) = id { 
    // some computation here 
    return! foo(expr) } 

你可以检查一下,新版本的模式相匹配,但原来的一个没有。

+0

这摆脱了通过用户代码递归,但在我的实现这里http://lorgonblog.spaces.live.com/blog/cns!701679AD17B6D310!1772.entry它仍然StackOverflows通过解析器实现本身。我现在会被激发去研究'延续解析器'... – Brian 2010-06-29 18:31:55

+0

FParsec http://www.quanttec.com/fparsec/处理这个问题吗? – Brian 2010-06-29 18:33:00

+0

布赖恩,我也用你的博客系列作为学习资源。它帮助了很多。同时我将Mau的答案('seq')与我的解析器进行了比较。我猜想monad中的Delay方法是导入。但我真的不知道。 FParsec使用'while'...但我想使用功能性解决方案:D – PetPaulsen 2010-06-29 18:50:15

2

一般来说也能够let!感谢绑定写尾递归计算表达式(见12),甚至具有多至“延迟”机制。

在这种情况下,chainl1的最后一个陈述是让你进入角落的原因。