| Brian's profileInside F#BlogGuestbookNetwork | Help |
|
|
June 02 Catamorphisms, part sixOops!... I did it again. I completely botched a key aspect of my previous blog entry. Fortunately I have some alert readers who are keeping me honest. Whereas last time I had to correct a blunder about run-time performance, this time I have to correct my implementation because I failed to make it properly tail-recursive. It's a learning opportunity, both for you and for me! The problemLast time I showed this code for KFoldTree and Change5to0bst: let KFoldTree nodeF leafV tree = let rec Loop t = match t with | Node(x,left,right) -> nodeF x (fun k -> k (Loop left)) (fun k -> k (Loop right)) t | Leaf -> leafV t Loop tree // Change5to0bst : Tree<int> -> Tree<int> let Change5to0bst tree = KFoldTree (fun x kl kr t -> let Node(_,oldL,oldR) = t if x < 5 then kr (fun newR -> Node (x, oldL, newR)) elif x > 5 then kl (fun newL -> Node (x, newL, oldR)) else Node(0,oldL,oldR) ) (fun t -> t) tree and I looked very carefully at Change5to0bst to ensure that every call was a tail call. It is. The problem is, the "Loop" calls in KFoldTree are not tail calls! For example: (fun k -> k (Loop left)) Here, we will make a recursive call to "Loop", but when that call returns, there is still "more work to do" (we must pass that result to "k"). Thus, Loop is not a tail call here. Oops! The implication is that we must allocate a stack frame for the duration of the recursive call. And so if we write... // CreateZeroRightTree : int -> Tree<int> let CreateZeroRightTree size = let rec Loop t n = if (n < size) then Loop (Node(0,Leaf,t)) (n+1) else t Loop Leaf 0 // make a big tree of 2 million nodes all going to the right let bigTree = CreateZeroRightTree (2 * 1000 * 1000) // call our supposedly-tail-recursive function on it Change5to0bst bigTree ...sure enough - kaboom! StackOverflowException. Clearly I failed to test my code from my previous blog entry. The fixThe fix involves explicitly passing the continuations throughout the computation. The definition of KFold actually gets a little simpler, though the client code becomes slightly more complicated. Here's the new KFold: let KFoldTree nodeF leafV tree = let rec Loop t k = match t with | Node(x,left,right) -> nodeF x (Loop left) (Loop right) t k | Leaf -> leafV t k Loop tree (fun x -> x) Relative to the previous (broken) version, "Loop" takes an extra continuation parameter, and passed it as an extra parameter to the client functions ("nodeF" and "leafV"). Note that the "Loop" calls got simpler, since for example (fun k -> Loop left k) can just be written as (Loop left) thanks to currying. Here is how this affects the client: let Change5to0bst tree = KFoldTree (fun x kl kr t k -> let Node(_,oldL,oldR) = t if x < 5 then kr (fun newR -> k (Node(x, oldL, newR))) elif x > 5 then kl (fun newL -> k (Node(x, newL, oldR))) else k (Node(0,oldL,oldR)) ) (fun t k -> k t) tree The two client lambdas now take an extra final parameter "k", and everywhere that the client used to "return a final value", now we are calling the continuation "k" on that final value. Apart from that, the code is otherwise unchanged. The lessonTail recursion is subtle - especially when dealing with mutually recursive functions/lambdas. If you are going to try to be tail-recursive, test your code on large inputs to ensure you got it right! I failed to test the new "Eval" function (see below), so as to leave that as a good exercise for you to try - create some large data and find out if I got the definitions of KFoldExpr and Eval right! |
|
|