2016-10-20 2 views
9

J'ai une mise en œuvre naïve d'un gameloopexception de débordement de pile lors de l'utilisation des tuyaux en fonction récursive

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 

Cette mise en œuvre jette un StackOverflowException. Dans mon esprit, cela devrait être récursif. Je pourrais faire un travail autour comme ceci

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 

Donc ma question est pourquoi le premier exemple de code jette une exception de stackoverflow.

+2

Sur quelle plateforme courez-vous? –

+3

Pour clarifier: la version de travail que vous avez affichée fonctionne-t-elle? – Peter

+0

@FyodorSoikin Je cours sur Windows 10 en utilisant la version fsi 14.0.23413.0 – Xiol

Répondre

10

Je pense que la réponse est la même que dans le fil Vandroiy liens: lorsque vous avez

a 
|> f b 

alors en mode débogage le compilateur peut compiler cela comme une interprétation très littérale de

(f b) a 

et calculer explicitement f b en une seule étape et l'appliquer à a dans une deuxième étape. L'appel avec l'argument a est toujours un appel de queue, mais si le compilateur n'émet pas le préfixe d'opcode tail. (parce que les tailcalls sont désactivés, comme ils le sont par défaut en mode débogage), alors vous développez la pile avec l'explicit appeler et éventuellement obtenir un débordement de pile.

D'autre part, si vous écrivez

f b a 

directement alors cela ne se produit pas: le compilateur ne s'applique pas partiellement f, et au lieu de reconnaître que c'est un appel récursif direct et l'optimiser en une boucle (même en mode debug).

+0

Je pense que je vais aller avec ce vêtement. Il est le plus upvoted, il fait sens à moi et je peux le reproduire en studio visuel. – Xiol

5

Je pense c'est l'explication, mais j'encourage F experts # compilateur de peser si je suis hors de la base:

Le premier exemple n'est pas récursive parce que l'expression en position de queue est un appelez au |>, pas un appel à innerLoop.

Rappelant que |> est défini comme

let (|>) x f = f x 

si nous desugar la syntaxe du pipeline un peu, lorsque vous appelez

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

vous appelez efficacement:

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

comme expression de votre corps dans la fonction récursive. La notation infixée obscurcit un peu ce qui donne l'impression que innerLoop est en position de queue.

+0

Wow, @Vandroiy, c'est une monstrueuse question/réponse. Et il est assez difficile de l'analyser pour en tirer des conclusions, si ce n'est que, comme vous le dites, un pipeline peut créer des problèmes avec TCO. – Peter

+0

Oui, mais 'innerLoop delta' est dans la position de queue de' |> ', non? Donc 'innerLoop' devrait appeler' |> ', ce qui devrait alors annuler' innerLoop' ou est-ce que je manque quelque chose? – sepp2k

+0

Ah, désolé, j'ai supprimé mon commentaire juste avant qu'il ne soit répondu à. Pour plus de clarté, le commentaire supprimé faisait un lien vers [ce fil] (http://stackoverflow.com/questions/35722526/can-does-the-forward-pipe-operator-prevent-tail-call-optimization/35729493), qui J'ai maintenant posté dans les commentaires de la question. – Vandroiy