2016-10-20 1 views
9

Verwendung habe ich eine naive Implementierung eines gameloopStapelüberlaufausnahme, wenn Rohrleitungen in tail-rekursive Funktion

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 

Diese Implementierung löst eine Stackoverflow. In meinen Augen sollte dies rekursiv sein. Ich könnte eine Arbeit so machen

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 

Also meine Frage ist, warum das erste Codebeispiel eine Stackoverflow-Ausnahme auslöst.

+2

Auf welcher Plattform laufen Sie? –

+3

Um zu verdeutlichen: Funktioniert die von Ihnen gepostete Version der Arbeit in Ordnung? – Peter

+0

@FyodorSoikin Ich bin auf Windows 10 mit fsi Version 14.0.23413.0 – Xiol

Antwort

10

Ich denke, die Antwort ist das gleiche wie in dem Thread ist Vandroiy Links: Wenn Sie

a 
|> f b 

dann im Debug-Modus haben die Compiler dies wie eine sehr wörtliche Auslegung kompilieren können von

(f b) a 

und explizit f b in einem Schritt berechnen und in einem zweiten Schritt auf a anwenden. Der Aufruf mit dem Argument a ist immer noch ein Tail-Aufruf, aber wenn der Compiler das Opcode-Präfix tail. nicht ausgibt (da die tailcalls ausgeschaltet sind, wie sie standardmäßig im Debug-Modus sind), dann werden Sie den Stack mit dem expliziten erweitern Anruf und schließlich einen Stapelüberlauf bekommen.

Auf der anderen Seite, wenn Sie

f b a 

schreiben direkt dann dies nicht geschieht: der Compiler teilweise gilt nicht f, und stattdessen wird erkennen, dass dies ein direkter rekursive Aufruf und optimiert es in eine Schleife (sogar im Debug-Modus).

+0

Ich denke, ich werde mit diesem Awnser gehen. Es ist am meisten aufgewertet, es macht mir Sinn und ich kann es im visuellen Studio reproduzieren. – Xiol

5

ich denke, das ist die Erklärung, obwohl ermutige ich F # -Compiler Experten in wiegen, wenn ich off-base bin:

Das erste Beispiel ist nicht Schwanz-rekursiv, weil der Ausdruck in Schwanz Position a Anruf an |>, kein Anruf an innerLoop.

Hinweis darauf, dass |> definiert als

let (|>) x f = f x 

, wenn wir die Pipeline Syntax ein wenig desugar, wenn Sie

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

nennen sind Sie effektiv Aufruf:

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

als Ihr Körper Ausdruck in der rekursiven Funktion.

Die Infix-Notation verdunkelt dies ein wenig, so dass es aussieht wie innerLoop ist in Endposition.

+0

Wow, @Vandroiy, das ist ein monströser Frage/Antwort-Thread. Und es ist ziemlich schwer, es für Schlussfolgerungen zu analysieren, außer - wie Sie sagen - eine Pipeline kann mit TCO zu tun haben. – Peter

+0

Ja, aber 'innerLoop Delta' ist in der Endlage von' |> ', nein? Also sollte 'innerLoop'' '' '' '' '' '' '' '' '' '' '' _ '_' '' _ '' '' '_' _ '' '_' '' '' zurückliefern oder fehlt mir etwas? – sepp2k

+0

Ah, tut mir leid, ich habe meinen Kommentar gelöscht, bevor er beantwortet wurde. Zur Verdeutlichung verlinkte der gelöschte Kommentar zu [this thread] (http://stackoverflow.com/questions/35722526/can-does-the-forward-pipe-operator-prevent-tail-call-optimization/35729493) Ich habe jetzt auch in den Kommentaren der Frage gepostet. – Vandroiy

Verwandte Themen