使用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
函数的尾调用。
尾调用分析和优化是否通过此嵌套进行?是。
答案 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
。所以你有一个正确的尾递归算法,无论你需要经过多少次调整,你都不会打击堆栈。