Using the 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)
If I have a recursive function (convergence, Newton's method) with each iteration binding Writer results, I think this must not be tail recursive (even though it may look like it judged just from the recursive call):
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
because all the logs have to be held in a queue until the recursion ends (and then joined).
So one question is whether it's correct that the bind on Writer makes the recursion not a tail call. A second question is whether switching to the 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
will make this now be tail recursive:
//...
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
//...
Since the logic of Either's bind is short circuiting? Or could that adjustment >>=
hold onto a stack position?
EDIT:
So in light of the clear answer, I can clarify and answer my question--which now amounts to something like whether tail call position is "transitive". (1) The recursive call to nextStep
is a tail call in nextStep
. (2) The (initial) call to nextStep
is a tail call in bind
(of my Either
/Result
monad). (3) bind
is a tail call in the outer (recursive) solve
function.
Does the tail call analysis and optimization carry through this nesting? Yes.
>>=
operator as being a simple call tobind
, the lineadjustment >>= nextStep
is exactly equivalent tobind adjustment nextStep
: it will hold on to a stack frame if and only if the function does more work. Since here, thebind
call is in tail position, there won't be an extra stack frame. See my answer below for more. – rmunnlet (>>=) ma fm = Result.bind ma fm
. – RomnieEE