web-dev-qa-db-fra.com

Les langages fonctionnels sont-ils meilleurs à la récursivité?

TL; DR: les langages fonctionnels gèrent-ils mieux la récursivité que les langages non fonctionnels?

Je lis actuellement Code Complete 2. À un moment donné dans le livre, l'auteur nous met en garde contre la récursivité. Il dit que cela devrait être évité autant que possible et que les fonctions utilisant la récursivité sont généralement moins efficaces qu'une solution utilisant des boucles. À titre d'exemple, l'auteur a écrit une fonction Java utilisant la récursivité pour calculer la factorielle d'un nombre comme celui-ci (il se peut que ce ne soit pas exactement le même puisque je n'ai pas le livre avec moi pour le moment). ):

public int factorial(int x) {
    if (x <= 0)
        return 1;
    else
        return x * factorial(x - 1);
}

Ceci est présenté comme une mauvaise solution. Cependant, dans les langages fonctionnels, l'utilisation de la récursivité est souvent la façon préférée de faire les choses. Par exemple, voici la fonction factorielle dans Haskell utilisant la récursivité:

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

Et est largement accepté comme une bonne solution. Comme je l'ai vu, Haskell utilise très souvent la récursivité, et je n'ai vu nulle part où elle est mal vue.

Donc, ma question est essentiellement:

  • Les langages fonctionnels gèrent-ils mieux la récursivité que les langages non fonctionnels?

EDIT: Je suis conscient que les exemples que j'ai utilisés ne sont pas les meilleurs pour illustrer ma question. Je voulais juste souligner que Haskell (et les langages fonctionnels en général) utilisent la récursivité beaucoup plus souvent que les langages non fonctionnels.

42
marco-fiset

Oui, ils le font, mais pas seulement parce qu'ils peuvent, mais parce qu'ils doivent.

Le concept clé ici est pureté: une fonction pure est une fonction sans effets secondaires et sans état. Les langages de programmation fonctionnels adoptent généralement la pureté pour de nombreuses raisons, telles que le raisonnement sur le code et l'évitement des dépendances non évidentes. Certaines langues, notamment Haskell, vont même jusqu'à permettre niquement du code pur; tous les effets secondaires qu'un programme peut avoir (comme effectuer des E/S) sont déplacés vers un runtime non pur, gardant le langage lui-même pur.

Ne pas avoir d'effets secondaires signifie que vous ne pouvez pas avoir de compteurs de boucles (car un compteur de boucles constituerait un état mutable, et la modification d'un tel état serait un effet secondaire), donc le plus itératif qu'un langage fonctionnel pur puisse obtenir est d'itérer sur un - liste (cette opération est généralement appelée foreach ou map). La récursivité, cependant, est une correspondance naturelle avec une programmation fonctionnelle pure - aucun état n'est nécessaire pour récuser, sauf pour les arguments de fonction (en lecture seule) et une valeur de retour (en écriture seule).

Cependant, l'absence d'effets secondaires signifie également que la récursivité peut être implémentée plus efficacement et que le compilateur peut l'optimiser de manière plus agressive. Je n'ai pas étudié un tel compilateur en profondeur moi-même, mais pour autant que je sache, la plupart des compilateurs de langages de programmation fonctionnels effectuent une optimisation des appels de queue, et certains peuvent même compiler certains types de constructions récursives en boucles dans les coulisses.

37
tdammers

Vous comparez récursivité et itération. Sans élimination de l'appel de queue , l'itération est en effet plus efficace car il n'y a pas d'appel de fonction supplémentaire. De plus, l'itération peut durer indéfiniment, alors qu'il est possible de manquer d'espace de pile à partir d'un trop grand nombre d'appels de fonction.

Cependant, l'itération nécessite de changer un compteur. Cela signifie qu'il doit y avoir une variable mutable , ce qui est interdit dans un cadre purement fonctionnel. Les langages fonctionnels sont donc spécialement conçus pour fonctionner sans nécessiter d'itération, d'où les appels de fonction rationalisés.

Mais rien de tout cela ne explique pourquoi votre exemple de code est si élégant. Votre exemple illustre une propriété différente, qui est correspondance de modèle . C'est pourquoi l'échantillon Haskell n'a pas de conditions explicites. En d'autres termes, ce n'est pas la récursivité rationalisée qui rend votre code petit; c'est la correspondance des motifs.

18
chrisaycock

Techniquement non, mais pratiquement oui.

La récursivité est beaucoup plus courante lorsque vous adoptez une approche fonctionnelle du problème. En tant que tels, les langages conçus pour utiliser une approche fonctionnelle incluent souvent des fonctionnalités qui rendent la récursivité plus facile/meilleure/moins problématique. Du haut de ma tête, il y en a trois:

  1. Tail Call Optimization. Comme souligné par d'autres affiches, les langages fonctionnels nécessitent souvent un TCO.

  2. Évaluation paresseuse. Haskell (et quelques autres langues) est évalué paresseusement. Cela retarde le "travail" réel d'une méthode jusqu'à ce qu'il soit nécessaire. Cela a tendance à conduire à des structures de données plus récursives et, par extension, à des méthodes récursives pour y travailler.

  3. Immuabilité. La majorité des choses avec lesquelles vous travaillez dans les langages de programmation fonctionnels sont immuables. Cela facilite la récursivité car vous n'avez pas à vous soucier de l'état des objets au fil du temps. Vous ne pouvez pas faire changer une valeur sous vous par exemple. De plus, de nombreux langages sont conçus pour détecter fonctions pures . Étant donné que les fonctions pures n'ont pas d'effets secondaires, le compilateur a beaucoup plus de liberté sur l'ordre dans lequel les fonctions s'exécutent et sur d'autres optimisations.

Aucune de ces choses n'est vraiment spécifique aux langages fonctionnels par rapport aux autres, donc elles ne sont pas simplement meilleures parce qu'elles sont fonctionnelles. Mais parce qu'elles sont fonctionnelles, les décisions de conception prises tendent vers ces fonctionnalités car elles sont plus utiles (et leurs inconvénients moins problématiques) lors de la programmation fonctionnelle.

5
Telastyn

La seule raison technique que je connaisse est que certains langages fonctionnels (et certains langages impératifs si je me souviens) ont ce qu'on appelle optimisation des appels de queue qui permet à une méthode récursive de ne pas augmenter la taille de la pile à chaque récursif (c'est-à-dire que l'appel récursif remplace plus ou moins l'appel en cours sur la pile).

Notez que cette optimisation ne fonctionne pas sur tout appel récursif , uniquement les méthodes récursives d'appel (c'est-à-dire les méthodes qui ne maintiennent pas l'état à la heure de l'appel récursif)

1
Steven Evers

Haskell et d'autres langages fonctionnels utilisent généralement une évaluation paresseuse. Cette fonctionnalité vous permet d'écrire des fonctions récursives sans fin.

Si vous écrivez une fonction récursive sans définir un cas de base où la récursivité se termine, vous vous retrouvez avec des appels infinis à cette fonction et stackoverflow.

Haskell prend également en charge les optimisations d'appel de fonction récursives. Dans Java chaque appel de fonction s'empilerait et provoquerait une surcharge.

Alors oui, les langages fonctionnels gèrent mieux la récursivité que les autres.

1
Mert Akcakaya

Vous aurez envie de regarder La collecte des ordures est rapide, mais une pile est plus rapide , un document sur l'utilisation de ce que les programmeurs C considéreraient comme un "tas" pour les cadres de pile en C. compilé. Je crois que le l'auteur a bricolé avec Gcc pour le faire. Ce n'est pas une réponse définitive, mais cela pourrait vous aider à comprendre certains des problèmes de récursivité.

Le langage de programmation Alef , qui accompagnait le Plan 9 de Bell Labs, avait une instruction "devenu" (voir la section 6.6.4 de cette référence ). C'est une sorte d'optimisation explicite de la récursivité des appels de queue. Le "mais il utilise la pile d'appels!" l'argument contre la récursivité pourrait potentiellement être supprimé.

1
Bruce Ediger

TL; DR: Oui, ils le font
La récursivité est un outil clé dans la programmation fonctionnelle et donc beaucoup de travail a été fait pour optimiser ces appels. Par exemple, R5RS requiert (dans la spécification!) Que toutes les implémentations gèrent les appels de récursion de queue non liés sans que le programmeur ne se soucie du débordement de pile. À titre de comparaison, par défaut, le compilateur C ne fera même pas une optimisation évidente des appels de queue (essayez une inversion récursive d'une liste chaînée) et après certains appels, le programme se terminera (Le compilateur optimisera, cependant, si vous utilisez - O2).

Bien sûr, dans les programmes horriblement écrits, comme le fameux exemple fib qui est exponentiel, le compilateur a peu ou pas d'options pour faire sa "magie". Il faut donc veiller à ne pas entraver les efforts du compilateur en optimisation.

EDIT: Par l'exemple fib, je veux dire ce qui suit:

(define (fib n)
 (if (< n 3) 1 
  (+ (fib (- n 1)) (fib (- n 2)))
 )
)
0
K.Steff

Les langages fonctionnels sont meilleurs pour deux types de récursions très spécifiques: la récursion de queue et la récursion infinie. Ils sont tout aussi mauvais que d'autres langues pour d'autres types de récursivité, comme votre exemple factorial.

Cela ne veut pas dire qu'il n'y a pas d'algorithmes qui fonctionnent bien avec une récursivité régulière dans les deux paradigmes. Par exemple, tout ce qui nécessite de toute façon une structure de données de type pile, comme une recherche d'arbre en profondeur d'abord, est plus simple à implémenter avec la récursivité.

La récursivité revient plus souvent avec la programmation fonctionnelle, mais elle est également largement surutilisée, en particulier par les débutants ou dans les tutoriels pour les débutants, peut-être parce que la plupart des débutants en programmation fonctionnelle ont déjà utilisé la récursivité auparavant en programmation impérative. Il existe d'autres constructions de programmation fonctionnelles, comme les compréhensions de liste, les fonctions d'ordre supérieur et d'autres opérations sur les collections, qui sont généralement beaucoup mieux adaptées conceptuellement, pour le style, la concision, l'efficacité et la capacité d'optimisation.

Par exemple, la suggestion de delnan de factorial n = product [1..n] n'est pas seulement plus concis et plus facile à lire, il est également hautement parallélisable. Idem pour utiliser un fold ou reduce si votre langue n'a pas déjà un product déjà intégré. La récursivité est la solution de dernier recours pour ce problème. La principale raison pour laquelle vous le voyez résolu récursivement dans les didacticiels est comme un point de départ avant d'arriver à de meilleures solutions, pas comme un exemple de meilleure pratique.

0
Karl Bielefeldt