3

I'm still trying to implement 2-3 finger trees and I made good progress (repository). While doing some benchmarks I found out that my quite basic toList results in a StackOverflowException when the tree ist quite large. At first I saw an easy fix and made it tail-recursive.

Unfortunately, it turned out that toList wasn't the culprit but viewr was:

/// Return both the right-most element and the remaining tree (lazily).
let rec viewr<'a> : FingerTree<'a> -> View<'a> = function
    | Empty -> Nil
    | Single x -> View(x, lazyval Empty)
    | Deep(prefix, deeper, One x) ->
        let rest = lazy (
            match viewr deeper.Value with
            | Nil ->
                prefix |> Digit.promote
            | View (node, lazyRest) ->
                let suffix = node |> Node.toList |> Digit.ofList
                Deep(prefix, lazyRest, suffix)
        )
        View(x, rest)
    | Deep(prefix, deeper, Digit.SplitLast(shorter, x)) ->
        View(x, lazy Deep(prefix, deeper, shorter))
    | _ -> failwith Messages.patternMatchImpossible

Looking for the only recursive call it is obvious that this is is not tail-recursive. Somehow I hoped this problem wouldn't exist because that call is wrapped in a Lazy which IMHO is similar to a continuation.

I heard and read of continuations but so far never (had to) use(d) them. I guess here I really need to. I've been staring at the code for quite some time, putting function parameters here and there, calling them other places… I'm totally lost!

How can this be done?


Update: The calling code looks like this:

/// Convert a tree to a list (left to right).
let toList tree =
    let rec toList acc tree =
        match viewr tree with
        | Nil -> acc
        | View(head, Lazy tail) -> tail |> toList (head::acc)
    toList [] tree

Update 2: The code that caused the crash is this one.

let tree = seq {1..200000} |> ConcatDeque.ofSeq
let back = tree |> ConcatDeque.toList

The tree get built fine, I checked and it is only 12 levels deep. It's the call in line 2 that triggered the overflow.


Update 3: kvb was right, that pipe issue I ran into before has something to do with this. Re-testing the cross product of debug/release and with/without pipe it worked in all but one case: debug mode with the pipe operator crashed. The behavior was the same for 32 vs. 64 bit.

I'm quite sure that I was running release mode when posting the question but today it's working. Maybe there was some other factor… Sorry about that.


Although the crash is solved, I'm leaving the question open out of theoretical interest. After all, we're here to learn, aren't we?

So let me adapt the question:
From looking at the code, viewr is definitely not tail-recursive. Why doesn't it always blow up and how would one rewrite it using continuations?

Community
  • 1
  • 1
primfaktor
  • 2,831
  • 25
  • 34
  • Does this help? http://blog.ploeh.dk/2015/12/22/tail-recurse – Mark Seemann Nov 09 '16 at 15:50
  • 1
    The laziness does mean that there's effectively no recursive call at all - the problem is the interaction between this function and its caller(s). Could you also provide the calling code? – kvb Nov 09 '16 at 16:49
  • @kvb: Done. In case you need to see more, the whole repo is linked at the beginning. – primfaktor Nov 09 '16 at 17:09
  • Could you also include an argument that causes the exception? And what's your runtime environment? – kvb Nov 09 '16 at 18:29
  • 1
    In F# Interactive (x64) on Windows, I'm able to call `toList` as is on a tree with 1M elements without getting a stack overflow. – kvb Nov 09 '16 at 20:36
  • Another update. For the exact settings on that machine, I'd have to get back to you tomorrow. – primfaktor Nov 09 '16 at 20:41
  • That code also works for me in my environment. – kvb Nov 09 '16 at 20:54
  • 3
    Perhaps you're hitting the same issue as you did [here](https://stackoverflow.com/questions/35722526/can-does-the-forward-pipe-operator-prevent-tail-call-optimization)? Try removing the pipe. – kvb Nov 09 '16 at 21:46

1 Answers1

2

Calling viewr never results in an immediate recursive call to viewr (the recursive call is protected by lazy and is not forced within the remainder of the call to viewr), so there's no need to make it tail recursive to prevent the stack from growing without bound. That is, a call to viewr creates a new stack frame which is then immediately popped when viewr's work is done; the caller can then force the lazy value resulting in a new stack frame for the nested viewr call, which is then immediately popped again, etc., so repeating this process doesn't result in a stack overflow.

kvb
  • 54,864
  • 2
  • 91
  • 133