计算表达式中的递归函数

时间:2010-06-29 14:33:20

标签: f# tail-recursion computation-expression

首先是一些背景知识。我目前正在学习一些关于monadic解析器组合的东西。当我尝试从this paper转移'chainl1'功能时(第16-17页),我提出了这个解决方案:

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

2 个答案:

答案 0 :(得分: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) }

正如您可以检查的那样,新版本与此模式匹配,但原始版本没有。

答案 1 :(得分:2)

通常,由于“延迟”机制,即使有多个let!绑定,也可以编写尾递归计算表达式(请参阅12)。

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