具有monadic绑定的F#尾递归

时间:2017-09-05 03:34:06

标签: 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 

with bind:

let inline bind ma fm =
    let (Writer (a, log1)) = ma 
    let mb = fm a
    let (Writer (b, log2)) = mb
    let sum = ( ^w : (static member add :  ^w * ^w -> ^w) (log1, log2) )
    Writer (b, sum)

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

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

        adjustment >>= nextStep

    sovle params.guess 1

因为所有日志都必须保存在队列中,直到递归结束(然后加入)。

所以有一个问题是,Writer上的绑定是否使递归不是尾调用是否正确。第二个问题是是否切换到Either monad:

type Result<'e, 'a> = 
    | Ok of 'a
    | Err of 'e

with bind:

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
//...

既然Either的绑定逻辑是短路的?或者adjustment >>=可以保持堆叠位置吗?

编辑:

因此,根据明确的答案,我可以澄清并回答我的问题 - 现在的问题就像是尾部呼叫位置是否是“传递性的”。 (1)对nextStep的递归调用是nextStep中的尾调用。 (2)nextStep的{​​初始)呼叫是bind(我的Either / Result monad)中的尾部呼叫。 (3)bind是外部(递归)solve函数的尾调用。

尾调用分析和优化是否通过此嵌套进行?是。

1 个答案:

答案 0 :(得分:3)

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

你是正确的bind Writer函数不能以尾递归的方式调用它的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)

这就是我需要关注的全部内容。因为对fm的调用不是bind函数的最后一件事(即,它不在尾部位置),所以我知道该调用不是尾递归并将耗尽堆栈帧。

现在让我们看一下bind的{​​{1}}实现:

Result

因此,在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! 的此实现中,对bind的调用是函数在该代码分支中执行的最后一项操作,因此fm的结果是fm的结果1}}。因此,编译器会将对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。所以你有一个正确的尾递归算法,无论你需要经过多少次调整,你都不会打击堆栈。