带一元绑定的F#尾递归

带一元绑定的F#尾递归,f#,bind,monads,tail-recursion,F#,Bind,Monads,Tail Recursion,使用Writer monad: type Writer< 'w, 'a when 'w : (static member add: 'w * 'w -> 'w) and 'w : (static member zero: unit -> 'w) > = | Writer of 'a * 'w 如果我有一个递归函数(收敛,牛顿方法)和每个迭代绑定结果,我认为这一定不是尾部递归(即使它看起来可能只是从递归调用判断):

使用Writer monad:

type Writer< 'w, 'a when 'w : (static member add: 'w * 'w -> 'w) 
                    and  'w : (static member zero: unit -> 'w) > = 

    | Writer of 'a * 'w 
如果我有一个递归函数(收敛,牛顿方法)和每个迭代绑定结果,我认为这一定不是尾部递归(即使它看起来可能只是从递归调用判断):

现在将使其成为尾部递归:

//...
        let (adjustment : Result<Error,float>) = getAdjustment params guess
        let nextStep adjustment = 
            if (abs adjustment) <= (abs params.tolerance) then
                Result.rtn guess
            elif iteration > params.maxIter then
                Result.fail Error.OverMaxIter
            else
                solve (guess + adjustment) (iteration + 1)

        adjustment >>= nextStep
//...
/。。。
let(调整:结果)=getAdjustment params guess
让下一步调整=
如果(abs调整)参数为最大值,则
Result.fail Error.OverMaxIter
其他的
求解(猜测+调整)(迭代+1)
调整>>=下一步
//...
因为任何一方的绑定逻辑都是短路?或者,该
调整>>=
是否可以保持堆栈位置

编辑:

所以,根据清晰的答案,我可以澄清并回答我的问题——这相当于尾部呼叫位置是否“可传递”。(1) 对
nextStep
的递归调用是
nextStep
中的尾部调用。(2) 对
nextStep
的(初始)调用是
bind
中的尾部调用(属于我的
或者
/
Result
monad)。(3)
bind
是外部(递归)
solve
函数中的尾部调用


尾部调用分析和优化是否进行了这种嵌套?对

判断函数调用是否为尾部递归实际上非常简单:只需查看调用函数在该调用之后是否需要执行其他工作,或者该调用是否处于尾部位置(即,这是函数执行的最后一项操作,该调用的结果也是调用函数返回的结果)。这可以通过简单的静态代码分析来完成,而不了解代码的作用——这就是为什么编译器能够这样做,并在它生成的.DLL中生成正确的
.tail
操作码

您正确地认识到,
Writer
bind
函数不能以尾部递归方式调用其
fm
参数——当您查看您在问题中编写的
bind
的实现时,这一点的证明非常简单:

let inline bind ma fm =
    let (Writer (a, log1)) = ma 
    let mb = fm a   // <--- here's the call
    let (Writer (b, log2)) = mb   // <--- more work after the call
    let sum = ( ^w : (static member add :  ^w * ^w -> ^w) (log1, log2) )
    Writer (b, sum)
因此在这个
bind
的实现中,对
fm
的调用是函数沿着代码分支所做的最后一件事,因此
fm
的结果就是
bind
的结果。因此,编译器将把对
fm
的调用转换为适当的尾部调用,并且不会占用堆栈帧

现在让我们看一个级别,在那里调用
bind
。(好的,您使用
>=
操作符,但我假设您已将其定义为
let(>>=)ma fm=bind ma fm
,或类似的东西。注意:如果您的定义与此明显不同,那么我下面的分析将不正确。)您对
bind
的调用如下所示:

let rec solve guess iteration =
    // Definition of `nextStep` that calls `solve` in tail position
    adjustment >>= nextStep
从编译器的角度来看,行
adjustment>>=nextStep
完全等同于
bind adjustment nextStep
。因此,为了进行尾部位置代码分析,我们将假设该行是
bind adjustment nextStep


假设这是
solve
的完整定义,并且下面没有您没有展示的其他代码,那么对
bind
的调用处于尾部位置,因此它不会占用堆栈帧。并且
bind
在尾部位置调用
fm
(这里是
nextStep
)。和
nextStep
在尾部位置调用
solve
。因此,您有一个正确的尾部递归算法,无论需要进行多少次调整,您都不会破坏堆栈。

假设您已将
>=
运算符定义为对
绑定的简单调用,行
adjustment>>=nextStep
完全等同于
bind adjustment nextStep
:当且仅当函数执行更多工作时,它将保留堆栈帧。因为在这里,
bind
调用处于尾部位置,所以不会有额外的堆栈帧。更多信息,请参见下面的答案。是的,正如您对操作员所说的:
let(>>=)ma fm=Result.bind ma fm
。分析得不错。谢谢我看到对最终位置的分析是如何贯穿所有范围的。它不仅仅关注递归声明函数调用的特定位置。帮了大忙。再次感谢。
let bind ma fm =
    match ma with 
    | Ok a  -> fm a
    | Err e -> Err e
//...
        let (adjustment : Result<Error,float>) = getAdjustment params guess
        let nextStep adjustment = 
            if (abs adjustment) <= (abs params.tolerance) then
                Result.rtn guess
            elif iteration > params.maxIter then
                Result.fail Error.OverMaxIter
            else
                solve (guess + adjustment) (iteration + 1)

        adjustment >>= nextStep
//...
let inline bind ma fm =
    let (Writer (a, log1)) = ma 
    let mb = fm a   // <--- here's the call
    let (Writer (b, log2)) = mb   // <--- more work after the call
    let sum = ( ^w : (static member add :  ^w * ^w -> ^w) (log1, log2) )
    Writer (b, sum)
let bind ma fm =
    match ma with 
    | Ok a  -> fm a  // <--- here's the call
    | Err e -> Err e // <--- not the same code branch
    // <--- no more work!
let rec solve guess iteration =
    // Definition of `nextStep` that calls `solve` in tail position
    adjustment >>= nextStep