2010-11-04 5 views
17

J'essaie de comprendre la récurrence de la queue chez Haskell. Je pense que je comprends ce que c'est et comment cela fonctionne mais je voudrais m'assurer que je ne fais pas de bêtises.Recursion de queue dans Haskell

Voici la définition factoriel "standard":

factorial 1 = 1 
factorial k = k * factorial (k-1) 

Lors de l'exécution, par exemple, factorial 3, ma fonction sera lui-même appeler 3 fois (donner ou prendre). Cela pourrait poser un problème si je voulais calculer factorielle 99999999 car je pourrais avoir un débordement de pile. Après que j'arrive à factorial 1 = 1 je devrai "revenir" dans la pile et multiplier toutes les valeurs, donc j'ai 6 opérations (3 pour appeler la fonction elle-même et 3 pour multiplier les valeurs).

Maintenant, je vous présente une autre possible mise en œuvre factoriel:

factorial 1 c = c 
factorial k c = factorial (k-1) (c*k) 

Celui-ci est récursive aussi. Il s'appellera 3 fois. Mais il n'a pas alors le problème de devoir "revenir" pour calculer les multiplications de tous les résultats, car je passe déjà le résultat en argument de la fonction.

Ceci est, pour ce que j'ai compris, ce que Tail Recursion est à propos. Maintenant, il semble un peu mieux que le premier, mais vous pouvez toujours avoir des débordements de pile aussi facilement. J'ai entendu dire que le compilateur de Haskell va convertir les fonctions Tail-Recursive en boucles en arrière-plan. Je suppose que c'est la raison pour laquelle il est payant de faire des fonctions récursives de la queue?

Si c'est la raison alors il n'y a absolument aucun besoin d'essayer de rendre les fonctions récursives si le compilateur ne va pas faire ce tour intelligent - ai-je raison? Par exemple, bien qu'en théorie le compilateur C# puisse détecter et convertir les fonctions récursives de la queue en boucles, je sais (du moins c'est ce que j'ai entendu) qu'à l'heure actuelle il ne le fait pas. Il n'y a donc absolument aucun intérêt à rendre les fonctions récursives de nos jours. Est-ce que c'est ça?

Merci!

+0

signale simplement que la définition factoriel "standard" est 'factoriel 0 = 1' – irrelephant

+1

Oui, je thoug ht de cela mais factorielle 1 = 1 est plus efficace. –

+9

Vous savez, sauvegarder une seule étape d'itération est probablement la dernière chose à prendre en compte lors du calcul des factoriels. En outre, si vous essayez de calculer 99999999! Je suis sûr que les dépassements de pile seront le moindre de vos problèmes. –

Répondre

35

Il y a deux problèmes ici. L'un est la récurrence de la queue en général, et l'autre est la façon dont Haskell gère les choses.

En ce qui concerne la récursivité de la queue, vous semblez avoir la définition correcte. La partie utile est, parce que seul le résultat final de chaque appel récursif est nécessaire, les appels antérieurs n'ont pas besoin d'être conservés sur la pile. Au lieu de "s'appeler", la fonction fait quelque chose de plus proche du "remplacement" lui-même, qui finit par ressembler à une boucle itérative. C'est une optimisation assez simple que les compilateurs décents fourniront généralement. Le deuxième numéro est évaluation paresseuse. Étant donné que Haskell n'évalue l'expression que selon les besoins, la récursion par défaut ne fonctionne pas de la manière habituelle. Au lieu de remplacer chaque appel au fur et à mesure, il construit une énorme pile imbriquée de "thunks", c'est-à-dire des expressions dont la valeur n'a pas encore été demandée. Si cette pile de thunk devient assez grande, elle produira en effet un débordement de pile.

Il y a en fait deux solutions en Haskell, en fonction de ce que vous devez faire:

  • Si le résultat est constitué des constructeurs de données imbriquées - comme la production d'une liste - alors vous voulez éviter récursion de la queue; placez plutôt la récursion dans l'un des champs du constructeur. Cela permettra au résultat d'être paresseux et ne provoquera pas de débordements de pile.Si le résultat consiste en une seule valeur, vous voulez l'évaluer strictement, de sorte que chaque étape de la récursivité soit forcée dès que la valeur finale est requise. Cela donne la pseudo-itération habituelle que vous attendez d'une récursion de queue.

De plus, gardez à l'esprit que GHC est sacrément intelligent et, si vous compilez avec des optimisations, il sera souvent repérer les endroits où l'évaluation doit être stricte et prendre soin d'elle pour vous. Cela ne fonctionnera pas dans GHCi, cependant.

+1

+1: J'ajouterais seulement que l'optimisation de la queue-call est ** fondamentale **, pour des raisons évidentes, sur des langages fonctionnels purs comme Haskell (mais un peu inutile dans les langages mixtes ou purs impératifs comme C# ou Python). – rsenna

+3

@rsenna: Je ne dirais pas inutile, il est simplement plus facile de contourner le problème lorsque la version optimisée du cas le plus simple est une primitive de langage. Le TCO est toujours strictement supérieur, en ce sens que vous pouvez, par exemple, avoir deux fonctions qui s'appellent l'un l'autre ou quelque chose de plus compliqué. –

+1

@camccann: Ne vous méprenez pas, j'adore les langages fonctionnels ... Je pense que ce que j'essaie de dire, c'est que, dans les langages qui ont des boucles impératives, la présence de TCO serait juste rendre les choses plus confuses ... Même n'étant pas un programmeur Python moi-même, je suis d'accord avec le Zen de Python, en particulier "Il devrait y avoir une - et de préférence une seule - façon évidente de le faire". Laissez tail-call être utilisé uniquement sur les langages qui requièrent une récursion comme seule construction en boucle, c'est tout ce que je dis. – rsenna

7

Vous devez utiliser les mécanismes intégrés, alors vous ne devez pas penser à des façons de rendre votre récursive fonction

fac 0 = 1 
fac n = product [1..n] 

ou si le produit n'a pas été déjà défini:

fac n = foldl' (*) 1 [1..n] 

(voir http://www.haskell.org/haskellwiki/Foldr_Foldl_Foldl%27 dont pli ... version à utiliser)

+1

Cela semble à peine le point de la question. La question concerne la récurrence de la queue. -1 –

+1

La réponse de camccann est déjà bonne. Je ne sais pas comment vous voyez cela, mais je suis toujours heureux si je reçois ** à la fois ** ma réponse à la question, et quelques informations supplémentaires, des considérations ou des critiques. Et au lieu de voter en bas, pourquoi n'écrivez-vous pas simplement une réponse plus utile? – Landei

+8

Vous vous rendez compte que sans les optimisations, 'foldl' va provoquer des débordements de pile sur les grandes listes, n'est-ce pas? Cela semble un peu ironique dans son contexte. –