Efficacité d'une programmation purement fonctionnelle

397

Quelqu'un sait-il quel est le pire ralentissement asymptotique possible qui peut se produire lors de la programmation purement fonctionnelle et non impérative (c.-à-d. Autoriser les effets secondaires)?

Clarification à partir du commentaire d'itowlson : y a-t-il un problème pour lequel le meilleur algorithme non destructif connu est asymptotiquement pire que le meilleur algorithme destructif connu, et si oui de combien?

Opter
la source
6
Identique à la programmation impérative, quelle qu'elle soit.
R. Martinho Fernandes
3
@jldupont: Pour renvoyer le résultat d'un calcul bien sûr. Il existe de nombreux programmes sans effets secondaires. Ils ne peuvent tout simplement pas faire autre chose que calculer leur entrée. Mais c'est toujours utile.
jalf
24
Je peux le rendre aussi mauvais que vous le souhaitez, en écrivant mal mon code fonctionnel! * sourire * Je pense que ce que vous demandez est "y a-t-il un problème pour lequel le meilleur algorithme non destructif connu est asymptotiquement pire que le meilleur algorithme destructif connu, et si oui de combien?" ... est-ce vrai ?
Itowlson
2
pourriez-vous donner un exemple du type de ralentissement qui vous intéresse? votre question est un peu vague.
Peter Recore
5
Un utilisateur a supprimé sa réponse, mais il a affirmé que la version fonctionnelle du problème des 8 reines fonctionnait en plus d'une minute pour n = 13. Il a admis que ce n'était pas "très bien écrit", alors j'ai décidé d'écrire ma propre version du 8 reines en F #: pastebin.com/ffa8d4c4 . Inutile de dire que mon programme purement fonctionnel calcule n = 20 en un peu plus d'une seconde.
Juliette le

Réponses:

531

Selon Pippenger [1996] , lorsque l'on compare un système Lisp qui est purement fonctionnel (et a une sémantique d'évaluation stricte, pas paresseux) à un système qui peut muter des données, un algorithme écrit pour le Lisp impur qui s'exécute en O ( n ) peut être traduit à un algorithme en Lisp pur qui s'exécute en temps O ( n log n ) (basé sur les travaux de Ben-Amram et Galil [1992] sur la simulation de la mémoire à accès aléatoire en utilisant uniquement des pointeurs). Pippenger établit également qu'il existe des algorithmes pour lesquels c'est le mieux que vous puissiez faire; il y a des problèmes qui sont O ( n ) dans le système impur qui sont Ω ( n log n ) dans le système pur.

Il y a quelques mises en garde à faire sur ce document. Le plus important est qu'il ne traite pas des langages fonctionnels paresseux, tels que Haskell. Bird, Jones et De Moor [1997] démontrent que le problème construit par Pippenger peut être résolu dans un langage fonctionnel paresseux en temps O ( n ), mais ils n'établissent pas (et pour autant que je sache, personne ne l'a) si ou pas un langage fonctionnel paresseux ne peut résoudre tous les problèmes dans le même temps d'exécution asymptotique qu'un langage avec mutation.

Le problème construit par Pippenger nécessite que Ω ( n log n ) soit spécifiquement construit pour atteindre ce résultat, et n'est pas nécessairement représentatif des problèmes pratiques du monde réel. Il y a quelques restrictions sur le problème qui sont un peu inattendues, mais nécessaires pour que la preuve fonctionne; en particulier, le problème nécessite que les résultats soient calculés en ligne, sans pouvoir accéder aux futures entrées, et que l'entrée soit constituée d'une séquence d'atomes provenant d'un ensemble illimité d'atomes possibles, plutôt que d'un ensemble de taille fixe. Et l'article établit uniquement des résultats (borne inférieure) pour un algorithme impur de temps de parcours linéaire; pour les problèmes qui nécessitent un temps d'exécution plus long, il est possible que le O supplémentaire (log n), le facteur observé dans le problème linéaire peut être «absorbé» au cours d'opérations supplémentaires nécessaires pour les algorithmes avec des temps d'exécution plus longs. Ces clarifications et questions ouvertes sont explorées brièvement par Ben-Amram [1996] .

En pratique, de nombreux algorithmes peuvent être implémentés dans un langage fonctionnel pur avec la même efficacité que dans un langage avec des structures de données mutables. Pour une bonne référence sur les techniques à utiliser pour implémenter efficacement des structures de données purement fonctionnelles, voir "Purely Functional Data Structures" de Chris Okasaki [Okasaki 1998] (qui est une version étendue de sa thèse [Okasaki 1996] ).

Quiconque a besoin d'implémenter des algorithmes sur des structures de données purement fonctionnelles doit lire Okasaki. Vous pouvez toujours obtenir au pire un ralentissement O (log n ) par opération en simulant la mémoire mutable avec un arbre binaire équilibré, mais dans de nombreux cas, vous pouvez faire beaucoup mieux que cela, et Okasaki décrit de nombreuses techniques utiles, des techniques amorties aux techniques réelles. ceux qui effectuent le travail amorti progressivement. Les structures de données purement fonctionnelles peuvent être un peu difficiles à travailler et à analyser, mais elles offrent de nombreux avantages, tels que la transparence référentielle, qui sont utiles dans l'optimisation du compilateur, dans l'informatique parallèle et distribuée et dans la mise en œuvre de fonctionnalités telles que la gestion des versions, l'annulation et la restauration.

Notez également que tout cela ne traite que des temps de fonctionnement asymptotiques. De nombreuses techniques pour implémenter des structures de données purement fonctionnelles vous donnent un certain ralentissement constant des facteurs, en raison de la tenue de livres supplémentaire nécessaire pour qu'elles fonctionnent et des détails d'implémentation de la langue en question. Les avantages des structures de données purement fonctionnelles peuvent l'emporter sur ces ralentissements constants des facteurs, vous devrez donc généralement faire des compromis en fonction du problème en question.

Références

Brian Campbell
la source
50
Pippinger est l'autorité incontestée sur cette question. Mais nous devons souligner que ses résultats sont théoriques et non pratiques. Quand il s'agit de rendre les structures de données fonctionnelles pratiques et efficaces, vous ne pouvez pas faire mieux qu'Okasaki.
Norman Ramsey
6
itowlson: Je dois admettre que je n'ai pas lu suffisamment de Pippenger pour répondre à votre question; il a été publié dans un journal à comité de lecture, cité par Okasaki, et j'en ai lu suffisamment pour déterminer que ses affirmations sont pertinentes pour cette question, mais pas assez pour comprendre la preuve. La conclusion immédiate que j'obtiens pour les conséquences réelles est qu'il est trivial de convertir un algorithme impur O ( n ) en un algorithme O ( n log n ) pur, en simulant simplement la mémoire modifiable à l'aide d'un arbre binaire équilibré. Il y a des problèmes qui ne peuvent pas faire mieux que cela; Je ne sais pas si elles sont purement théoriques.
Brian Campbell
3
Le résultat de Pippenger fait deux hypothèses importantes qui limitent sa portée: il considère les calculs "en ligne" ou "réactifs" (pas le modèle habituel d'un calcul mappant les entrées finies à une seule sortie) et les calculs "symboliques" où les entrées sont des séquences de des atomes, qui ne peuvent être testés que pour l'égalité (c'est-à-dire que l'interprétation de l'entrée est extrêmement primitive).
Chris Conway
2
Très bonne réponse; Je voudrais ajouter que pour les langages purement fonctionnels, il n'y a pas de modèle universellement convenu pour la complexité informatique, alors que dans le monde impur, la machine RAM à coût unitaire est relativement standard (ce qui rend la comparaison des choses plus difficile). Notez également que la limite supérieure d'une différence Lg (N) en pur / impur peut être expliquée intuitivement très facilement en regardant une implémentation de tableaux dans un langage pur (cela coûte lg (n) par opération (et vous obtenez l'historique)) .
user51568
4
Point important: La traduction minutieuse d'une spécification purement fonctionnelle en une implémentation purement fonctionnelle efficace plus compliquée n'a que peu d'avantages si vous devez éventuellement - automatiquement ou manuellement - la traduire en un code impur encore plus efficace. L'impureté n'est pas si grave si vous pouvez la garder dans une cage, par exemple en la verrouillant dans une fonction sans effet secondaire externe.
Robin Green
44

Il existe en effet plusieurs algorithmes et structures de données pour lesquels aucune solution purement fonctionnelle asymptotiquement efficace (celle implémentable en calcul lambda pur) n'est connue, même avec paresse.

  • La trouvaille syndicale susmentionnée
  • Tables de hachage
  • Tableaux
  • Quelques algorithmes de graphe
  • ...

Cependant, nous supposons que dans les langues "impératives", l'accès à la mémoire est O (1) alors qu'en théorie cela ne peut pas être aussi asymptotique (c'est-à-dire pour des tailles de problème illimitées) et l'accès à la mémoire dans un énorme ensemble de données est toujours O (log n) , qui peut être émulé dans un langage fonctionnel.

En outre, nous devons nous rappeler qu'en réalité, tous les langages fonctionnels modernes fournissent des données mutables, et Haskell les fournit même sans sacrifier la pureté (la monade ST).

jkff
la source
3
Si l'ensemble de données tient dans la mémoire physique, l'accès à celui-ci est O (1) en ce sens qu'il est possible de trouver une limite supérieure absolue sur la durée de lecture d'un élément. Si l'ensemble de données ne fonctionne pas, vous parlez d'E / S et ce sera de loin le facteur dominant, mais le programme est écrit.
Donal Fellows
Eh bien, bien sûr, je parle des opérations O (log n) d'accès à la mémoire externe. Cependant, dans tous les cas, je parlais de bs: la mémoire externe peut également être adressable en O (1) ...
jkff
2
Je pense que l'une des plus grandes choses que la programmation impérative gagne par rapport à la programmation fonctionnelle est la capacité à contenir des références à de nombreux aspects distincts d'un état, et à générer un nouvel état de sorte que toutes ces références pointent vers les aspects correspondants du nouvel état. L'utilisation de la programmation fonctionnelle nécessiterait le remplacement des opérations de déréférencement direct par des opérations de recherche pour trouver l'aspect approprié d'une version particulière de l'état général actuel.
supercat
Même le modèle de pointeur (accès mémoire O (log n), en gros) n'est pas physiquement réaliste à des échelles extrêmement grandes. La vitesse de la lumière limite la rapidité avec laquelle différents équipements informatiques peuvent communiquer entre eux, alors que l'on pense actuellement que la quantité maximale d'informations pouvant être conservée dans une région donnée est limitée par sa surface.
dfeuer
36

Cet article prétend que les implémentations purement fonctionnelles connues de l'algorithme de recherche d'union ont toutes une complexité asymptotique pire que celle qu'elles publient, qui a une interface purement fonctionnelle mais utilise des données mutables en interne.

Le fait que d'autres réponses prétendent qu'il ne peut jamais y avoir de différence et que, par exemple, le seul "inconvénient" du code purement fonctionnel est qu'il peut être parallélisé vous donne une idée de la connaissance / objectivité de la communauté de programmation fonctionnelle sur ces questions. .

ÉDITER:

Les commentaires ci-dessous soulignent qu'une discussion biaisée des avantages et des inconvénients de la programmation fonctionnelle pure peut ne pas provenir de la «communauté de programmation fonctionnelle». Bon point. Peut-être que les avocats que je vois sont, pour citer un commentaire, «analphabètes».

Par exemple, je pense que ce billet de blog est écrit par quelqu'un qui pourrait être considéré comme représentatif de la communauté de programmation fonctionnelle, et puisqu'il s'agit d'une liste de «points pour une évaluation paresseuse», ce serait un bon endroit pour mentionner tout inconvénient une programmation paresseuse et purement fonctionnelle pourrait avoir. Un bon endroit aurait été à la place du licenciement suivant (techniquement vrai, mais biaisé au point de ne pas être drôle):

Si une fonction stricte a une complexité O (f (n)) dans un langage strict, elle a également une complexité O (f (n)) dans un langage paresseux. Pourquoi s'inquiéter? :)

Pascal Cuoq
la source
4

Avec une limite supérieure fixe sur l'utilisation de la mémoire, il ne devrait pas y avoir de différence.

Esquisse de preuve: étant donné une limite supérieure fixe sur l'utilisation de la mémoire, on devrait être capable d'écrire une machine virtuelle qui exécute un jeu d'instructions impératif avec la même complexité asymptotique que si vous exécutiez réellement sur cette machine. En effet, vous pouvez gérer la mémoire mutable en tant que structure de données persistante, donnant à O (log (n)) la lecture et l'écriture, mais avec une limite supérieure fixe sur l'utilisation de la mémoire, vous pouvez avoir une quantité fixe de mémoire, ce qui provoque leur décroissance en O (1). Ainsi, l'implémentation fonctionnelle peut être la version impérative exécutée dans l'implémentation fonctionnelle de la machine virtuelle, et elles doivent donc toutes deux avoir la même complexité asymptotique.

Brian
la source
6
Une limite supérieure fixe sur l'utilisation de la mémoire n'est pas la façon dont les gens analysent ce genre de choses; vous supposez une mémoire arbitrairement grande mais finie. Lors de la mise en œuvre d'un algorithme, je m'intéresse à la façon dont il évoluera de l'entrée la plus simple jusqu'à n'importe quelle taille d'entrée arbitraire. Si vous fixez une limite supérieure fixe à l'utilisation de la mémoire, pourquoi ne fixez-vous pas également une limite supérieure fixe sur combien de temps vous autoriserez le calcul, et dites que tout est O (1)?
Brian Campbell
@Brian Campbell: C'est vrai. Je dis simplement que si vous le vouliez, vous pourriez ignorer la différence du facteur constant dans de nombreux cas dans la pratique. Il faudrait toujours être conscient de la différence lors d'un compromis entre la mémoire et le temps pour vous assurer que l'utilisation de m fois plus de mémoire diminue votre durée d'exécution d'au moins un facteur de log (m).
Brian