计算表达式和尾递归中的 F# 递归绑定

F# recursive bind in computational expressions and tail recursiveness

我正在尝试了解如何在计算表达式中正确调用递归函数并且不会出现堆栈溢出异常。据我了解,这是众所周知的问题,但仍然无法理解这个概念。也许有人对此有简单的解释或例子。

这是我的例子 我希望跟踪构建器具有类似于 seq 的行为,但不使用 seq monad 而不是使用其他一些,例如 option 和 return 仅来自递归循环的最新非 None 值.可能吗?

这只是示例,代码将 运行 无限但不应该出现 stackowerflow 异常

据我了解 Combine 方法中的堆栈溢出问题,代码只是在递归循环中继续调用 f() 函数,我想避免这种情况并使 此调用尾递归,即代码应在常规循环中编译。

type TraceBuilder() = 

    member __.Bind (m: int Option, f: int -> int Option) : int Option =
         match m with
         | Some(v) -> f v
         | None -> None

    member this.Yield(x) = Some x

    member this.YieldFrom(x) = x

    member __.Delay(f) = f

    member __.Run(f) = f()

    member __.Combine (a, f) = 
        match a with
        | Some _ -> a    
        | None -> f()    

let trace = new TraceBuilder()

let rec iterRec (xopt: int Option) = 
    trace {        
        yield! None
        let! x = xopt        
        yield! iterRec(Some(x + 1))
    }


[<EntryPoint>]
let main argv = 
    let x = iterRec(Some(0))
    //x = startFrom(0) |> Seq.take(5000) |> Seq.toList  |> ignore
    printfn "%A" x

在comp中思考代码。表达式应该被编译

let iterRec xopt = 
    combine (None, (fun () ->
              bind(xopt, fun x -> iterRec(Some(x+ 1)))

看起来在这种情况下 iterRec 调用不是尾递归的,那么为什么是 stackoveflow,是否可以实现这个逻辑尾递归?

阅读这些链接,仍然无法找到解决方案:

(How) can I make this monadic bind tail-recursive?

这里建议如何使用 FsControl 库解决问题,但是否可以使用正则计算表达式解决问题?

Recursive functions in computation expressions

Avoiding stack overflow (with F# infinite sequences of sequences)

https://fsharpforfunandprofit.com/posts/computation-expressions-builder-part5/

我删除了我认为对于该问题不必要的部分代码。请注意,我还发现您的 Combine 定义令人困惑。它可能很可爱,但它会让我完全失望,因为 Combine 的行为应该与 Bind 相似,因为两个操作被链接在一起。您的 Combine 操作关闭通常是 OrElse 操作。

无论如何:

module Trace =
  let treturn a = Some a
  let tbind a b =
      match a with
      | Some(v)  -> b v
      | None     -> None
  let (>>=) a b = tbind a b

open Trace

// Will throw on Debug (and most likely Mono)
let rec iterRec xopt l =
  xopt >>= fun x -> if x < l then iterRec(Some(x + 1)) l else Some x

[<EntryPoint>]
let main argv =
  let x = iterRec_(Some(0)) 100000
  printfn "%A" x
  0

iterRec 在调试中抛出 WhosebugException 并且不识别 .tail 属性的抖动。

通过查看反汇编的 iterRec(例如使用 ILSpy)可以更容易理解发生了什么`)

iterRec 等于:

public static FSharpOption<int> iterRec(FSharpOption<int> xopt, int l)
{
  return Program.Trace.tbind<int, int>(xopt, new Program.iterRec@13(l));
}


internal class iterRec@13 : FSharpFunc<int, FSharpOption<int>>
{
  public int l;

  internal iterRec@13(int l)
  {
    this.l = l;
  }

  public override FSharpOption<int> Invoke(int x)
  {
    if (x < this.l)
    {
      return Program.iterRec(FSharpOption<int>.Some(x + 1), this.l);
    }
    return FSharpOption<int>.Some(x);
  }
}

这两个函数相互递归,但在 Release 上构建 .tail 属性有助于 Jitter 避免增加堆栈。

反汇编为 IL 时看到 .tail 属性。

IL_0008: tail.
IL_000a: call class [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1<!!1> Program/Trace::tbind<int32, int32>(class [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1<!!0>, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0, class [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1<!!1>>)

不幸的是,并不是所有的 Jitters 都关心 .tail,这就是为什么我不愿依赖它并将 iterRec 重写为 F# 能够解包的尾递归函数:

let rec iterRec_ xopt l =
  // This F# unpacks into a while loop
  let rec loop xo =
    match xo with
    | Some x  -> if x < l then loop(Some(x + 1)) else xo
    | None    -> None
  loop xopt

ILSpy中检查此功能:

internal static FSharpOption<int> loop@17(int l, FSharpOption<int> xo)
{
  while (xo != null)
  {
    FSharpOption<int> fSharpOption = xo;
    int x = fSharpOption.Value;
    if (x >= l)
    {
      return xo;
    }
    int arg_1E_0 = l;
    xo = FSharpOption<int>.Some(x + 1);
    l = arg_1E_0;
  }
  return null;
}

不再递归此函数将在 Debug 抖动以及 mono.

上正常执行

另一种方法是实现一个蹦床模式,用堆栈 space 交换堆 space。