9

I have a naive implementation of a gameloop

let gameLoop gamestate =     
    let rec innerLoop prev gamestate =
        let now = getTicks()
        let delta = now - prev
        gamestate 
        |> readInput delta
        |> update delta
        |> render delta
        |> innerLoop delta             

    innerLoop 0L gamestate 

This implementation throws a stackoverflowexception. In my mind this should be tail recursive. I could make a work around like this

let gameLoop gamestate =     
    let rec innerLoop prev gamestate =
        let now = getTicks()
        let delta = now - prev
        let newState = gamestate 
            |> readInput delta
            |> update delta
            |> render delta

        innerLoop now newState

    innerLoop 0L gamestate  

So my question is why the first code example throws a stackoverflow exception.

Peter
  • 3,619
  • 3
  • 31
  • 37
Xiol
  • 170
  • 10
  • 2
    On which platform are you running? – Fyodor Soikin Oct 20 '16 at 20:35
  • 3
    To clarify: does the work-around version you posted work OK? – Peter Oct 20 '16 at 20:38
  • @FyodorSoikin I am running on Windows 10 using fsi version 14.0.23413.0 – Xiol Oct 20 '16 at 21:03
  • @Peter yes my workaround is working. – Xiol Oct 20 '16 at 21:04
  • 6
    [This question](http://stackoverflow.com/questions/35722526/can-does-the-forward-pipe-operator-prevent-tail-call-optimization/35729493) might be of interest here. In any case, it may be interesting if the error happens on a debug or release compile, and whether it matters if a debugger is attached. – Vandroiy Oct 20 '16 at 21:08

2 Answers2

10

I think the answer is the same as in the thread Vandroiy links: when you have

a
|> f b

then in debug mode the compiler may compile this like a very literal interpretation of

(f b) a

and explicitly calculate f b in one step and apply it to a in a second step. The call with argument a is still a tail call, but if the compiler doesn't emit the tail. opcode prefix (because tailcalls are turned off, as they are by default in debug mode), then you'll grow the stack with the explicit call and eventually get a stack overflow.

On the other hand, if you write

f b a

directly then this doesn't happen: the compiler does not partially apply f, and instead will recognize that this is a direct recursive call and optimize it into a loop (even in debug mode).

Community
  • 1
  • 1
kvb
  • 54,864
  • 2
  • 91
  • 133
  • I think I will go with this awnser. It is most upvoted, it makes sens to me and I can reproduce it in visual studio. – Xiol Oct 21 '16 at 11:59
5

I think this is the explanation, though I encourage F# compiler experts to weigh in if I'm off-base:

The first example is not tail-recursive because the expression in tail position is a call to |>, not a call to innerLoop.

Recalling that |> is defined as

let (|>) x f = f x

if we desugar the pipeline syntax a little bit, when you call

gamestate 
    |> readInput delta
    |> update delta
    |> render delta
    |> innerLoop delta

you're effectively calling:

|> (innerLoop delta) (|> (render delta) (|> (update delta) (|> (readInput delta) gamestate)))

as your body expression in the recursive function.

The infix notation obscures this a bit, making it look like innerLoop is in tail position.

Peter
  • 3,619
  • 3
  • 31
  • 37
  • Wow, @Vandroiy, that is a monstrous question/answer thread. And it's quite hard to parse it for conclusions, other than -- like you say -- a pipeline can mess with TCO. – Peter Oct 20 '16 at 21:08
  • Yeah, but `innerLoop delta` is in the tail position of `|>`, no? So `innerLoop` should tailcall `|>`, which should then tailcall `innerLoop` back or am I missing something? – sepp2k Oct 20 '16 at 21:08
  • Ah, sorry, I deleted my comment just before it was responded to. For clarity, the deleted comment was linking to [this thread](http://stackoverflow.com/questions/35722526/can-does-the-forward-pipe-operator-prevent-tail-call-optimization/35729493), which I've now also posted in the question's comments. – Vandroiy Oct 20 '16 at 21:15
  • 2
    Note that the actual definition is `let inline (|>) x f = f x` (see [prim-types.fs](https://github.com/Microsoft/visualfsharp/blob/master/src/fsharp/FSharp.Core/prim-types.fs)). Since the definition is `inline` there shouldn't be a call to the operator in the output at all. – kvb Oct 20 '16 at 21:18
  • @sepp2k (Questions of `|>` being inline aside) I don't think the F# compiler is sophisticated enough to infer that a call to a different function in tail position involves a call to the enclosing function and to then optimize it. Different compilers do different things of course: compilers for some languages allow tail-recursion _modulo_ cons, for instance. Tail call rules for F# are detailed at https://blogs.msdn.microsoft.com/fsharpteam/2011/07/08/tail-calls-in-f/ – Peter Oct 20 '16 at 21:26
  • 2
    @Peter I'm not talking about the F# compiler detecting that |> calls innerLoop back. I'm talking about it generating a tailcall instruction instead of a call instruction because the call to |> is a tail call. – sepp2k Oct 20 '16 at 21:29