gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-01 01:56:38 24 4
gpt4 key购买 nike

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

这是我的例子
我希望跟踪生成器的行为类似于 seq但不使用 seq monad 而是使用其他一些单子(monad),例如 option并从递归循环中仅返回最新的非 None 值。是否可以 ?

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

据我了解码合方法中的堆栈溢出问题,代码只是在递归循环中调用 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 lib 解决问题,但是否可以使用正则计算表达式解决问题?

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抛出 StackOverflowException在无法识别 .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 避免增加堆栈。

一看到 .tail反汇编时属性为 IL .
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 .

另一种方法是实现蹦床模式以用堆栈空间交换堆空间。

关于计算表达式中的 F# 递归绑定(bind)和尾递归,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40074078/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com