views:

190

answers:

3

This F# seq expression looks tail-recursive to me, but I'm getting stack overflow exceptions (with tail-calls enabled). Does anybody know what I'm missing?

let buildSecondLevelExpressions expressions =
    let initialState = vector expressions |> randomize
    let rec allSeq state = seq {
        for partial in state do
            if count partial = 1
            then yield Seq.head partial
            if count partial > 1 || (count partial = 1 && depth (Seq.head partial) <= MAX_DEPTH) then
                let allUns = partial
                                |> pick false 1
                                |> Seq.collect (fun (el, rr) -> (createExpUnaries el |> Seq.map (fun bn -> add rr bn)))
                let allBins = partial  // Careful: this case alone produces result recursivley only if |numbers| is even (rightly!).
                                |> pick false 2
                                |> Seq.collect (fun (el, rr) -> (createExpBinaries el |> Seq.map (fun bn -> add rr bn)))
                yield! allSeq (interleave allBins allUns)
    }
    allSeq initialState

If you're wondering, though it shouldn't be important, pick is used to generate combinations of elements in a sequence and interleave interleaves elements from 2 sequences. vector is a constructor for a ResizeArray.

+2  A: 

This is not going to be tail recursive because you could be calling recursively multiple times. To translate to a pseudo-code:

allSeq(state)
{
    foreach (partial in state)
    {
        if (...)
        {
            yield ...
        }
        if (...)
        {
            ...
            //this could be reached multiple times
            yield! allSeq(...)
        }
    }
}
Gideon Engelberth
Oohh yes, you're right. So dumb :-)
Mau
Yes, it will generate a tree...
Skilldrick
Now...how to make it TR...
Mau
+2  A: 

As Gideon pointed out, this is not tail-recursive, because you still have other elements in the 'state' list to process. Making this tail-recursive isn't straightforward, because you need some queue of elements that should be processed.

The following pseudo-code shows one possible solution. I added work parameter that stores the remaining work to be done. At every call, we process just the first element. All other elements are added to the queue. When we finish, we pick more work from the queue:

let rec allSeq state work = seq { 
    match state with 
    | partial::rest -> 
        // Yield single thing to the result - this is fine
        if count partial = 1 then yield Seq.head partial 
        // Check if we need to make more recursive calls...
        if count partial > 1 || (* ... *) then 
            let allUns, allBins = // ...
            // Tail-recursive call to process the current state. We add 'rest' to 
            // the collected work to be done after the current state is processed
            yield! allSeq (interleave allBins allUns) (rest :: work)
        else
            // No more processing for current state - let's take remaining
            // work from the 'work' list and run it (tail-recursively)
            match work with 
            | state::rest -> yield! allSeq state rest
            | [] -> () //completed
    | _ -> 
        // This is the same thing as in the 'else' clause above. 
        // You could use clever pattern matching to handle both cases at once
        match work with 
        | state::rest -> yield! allSeq state rest
        | [] -> () } //completed
Tomas Petricek
Nice! Thanks Tomas!
Mau
@Tomas Petricek -- is this a continuation? Seriously, I'm trying to understand the concept of a continuation and this looks like one to me.
Onorio Catenacci
Have you tested it? I think it is wrong: you should never recurse inside a `yield`...
Jon Harrop
@Jon: I didn't test it (it is pretty incomplete snippet), but I think that all recursive uses of `allSeq` are in tail-call position and are returned using `yield!` - the sequence expression compiler looks exactly for this case.
Tomas Petricek
@Onorio: Continuations are similar but a more general concept - instead of passing a queue of things that need to be done, you pass a function that should be run (in the example above, `work` would be function and we would execute it when we complete the current processing). There is a clearer explanation in my book (http://manning.com/petricek), but unfortunatelly, this part isn't available in the free sample chapters...
Tomas Petricek
@Tomas: Surely if that were true you would be able to enumerate `let rec xs() = seq {yield! xs()}` yet that stack overflows.
Jon Harrop
@Jon: I guess the optimization in the compiler works only for non-immediate recursive references (?); the following is fine: `let rec xs n = seq { yield n; yield! xs (n + 1) } `
Tomas Petricek
@Tomas: Wow, that is weird. I'd like to see a definition what exactly is in tail call position with a sequence expression, particularly before depending upon it like this. My general solution is to always use `Seq.unfold` instead of recursive sequence expressions.
Jon Harrop
A: 

I cannot find a definition of which calls inside a sequence expression are in tail position in F# so I would strongly recommend not writing code that depends upon the semantics of the current implementation, i.e. this is undefined behaviour.

For example, trying to enumerate (e.g. applying Seq.length) the following sequence causes a stack overflow:

let rec xs() = seq { yield! xs() }

but, as Tomas pointed out, the following does actually work:

let rec xs n = seq { yield n; yield! xs(n+1) }

My advice is to always replace recursive sequence expressions with Seq.unfold instead. In this case, you probably want to accumulate the work to be done (e.g. when you recurse into a left branch you push the right branch onto the stack in the accumulator).

FWIW, even the F# language reference gets this wrong. It gives the following code for flattening a tree:

type Tree<'a> =
   | Tree of 'a * Tree<'a> * Tree<'a>
   | Leaf of 'a

let rec inorder tree =
    seq {
      match tree with
          | Tree(x, left, right) ->
               yield! inorder left
               yield x
               yield! inorder right
          | Leaf x -> yield x
    } 

Their own code kills F# interactive with a stack overflow when fed a deep tree on the left.

Jon Harrop
I think you're wrong: yield!, let!, do! can be tail-recursive, just not in this case. +1 for Seq.unfold though, thanks.
Mau
@Mau: Tomas has given me a counter example where a recursive call can be in tail position inside a sequence expression. However, without any clue as to when this occurs I would absolutely avoid depending upon it.
Jon Harrop
@Jon: I think the case you posted could be a considered as a bug in F#, because all `yield!` in tail-call position should be optimized. I'll send a link to the F# team.
Tomas Petricek
@Jon: I don't understand what are you referring to when you say _"F# language reference gets this wrong"_. What does it get wrong? It doesn't claim that the solution doesn't stack-overflow (and I think it isn't a surprise for any functional programmers).
Tomas Petricek
@Jon: I think it depends on how you understand _computation expressions_. My understanding is that they are just like standard F# computations with some additional aspect. This understanding suggests that e.g. stack overflow can be caused by similar patterns as in regular F# code. However, I agree that this should be clearly documented in the F# docs. Redarding C# documentation - I of course don't expect that loop will break, but I also don't expect that C# compiler will magicaly resolve NullReferenceExceptions in places where I see they can occur in their samples.
Tomas Petricek
@Tomas: This is literally equivalent to a loop breaking in C#. I made a mistake and the code they gave does actually work on right recursion, just not on left recursion. I do not regard computation expressions as being equivalent to ordinary expressions. I understood them to be a monadic syntax where constructs like `let!` and `yield!` get compiled into continuations. Therefore, I would expect them to *always* turn into tail calls. Indeed, I cannot imagine why they are not tail calls (unless F# is doing something stupid like precomputing many elements at a time before yielding a single one).
Jon Harrop
@Jon, @Tomas - generally speaking, whether "tail recursion" works properly is at the discretion of the computation builder being used. I'd say that Jon's pathological example is indeed manifesting a compiler bug, since using the spec's definition of SequenceBuilder instead of `seq` leads to an infinite loop but not a stack overflow.
kvb