Comment les langages de programmation purement fonctionnels traitent-ils les données à évolution rapide?

22

Quelles structures de données pouvez-vous utiliser pour obtenir la suppression et le remplacement d'O (1)? Ou comment éviter les situations où vous avez besoin de ces structures?

mrpyo
la source
2
Pour ceux d'entre nous qui sont moins familiers avec les langages de programmation purement fonctionnels, pensez-vous que vous pourriez fournir un peu plus d'informations afin que nous comprenions quel est votre problème?
FrustratedWithFormsDesigner
4
@FrustratedWithFormsDesigner Les langages de programmation purement fonctionnels nécessitent que toutes les variables soient immuables, ce qui nécessite à son tour des structures de données qui créent de nouvelles versions d'elles-mêmes lorsqu'elles sont "modifiées".
Doval
5
Connaissez-vous le travail d'Okasaki sur les structures de données purement fonctionnelles?
2
Une possibilité est de définir une monade pour les données mutables (voir par exemple haskell.org/ghc/docs/4.08/set/sec-marray.html ). De cette façon, les données mutables sont traitées de la même manière que IO.
Giorgio
1
@CodesInChaos: cependant, ces structures immuables ont généralement beaucoup plus de frais généraux que les tableaux simples. En conséquence, il y a beaucoup de différence pratique. C'est pourquoi tout langage purement fonctionnel qui vise la programmation à usage général devrait avoir un moyen d'utiliser des structures mutables, d'une certaine manière compatible avec la sémantique pure. La STmonade de Haskell le fait très bien.
leftaroundabout

Réponses:

32

Il existe une vaste gamme de structures de données exploitant la paresse et d'autres astuces pour obtenir un temps constant amorti ou même (dans certains cas limités, comme les files d' attente ) des mises à jour à temps constant pour de nombreux types de problèmes. La thèse de doctorat de Chris Okasaki "Structures de données purement fonctionnelles" et le livre du même nom en sont un excellent exemple (peut-être le premier grand), mais le domaine a progressé depuis . Ces structures de données sont généralement non seulement purement fonctionnelles dans l'interface, mais peuvent également être implémentées en Haskell pur et dans des langages similaires, et sont entièrement persistantes.

Même sans aucun de ces outils avancés, de simples arbres de recherche binaires équilibrés fournissent des mises à jour logarithmiques, de sorte que la mémoire mutable peut être simulée avec au pire un ralentissement logarithmique.

Il existe d'autres options, qui peuvent être considérées comme de la triche, mais sont très efficaces en ce qui concerne l'effort de mise en œuvre et les performances réelles. Par exemple, les types linéaires ou les types d' unicité permettent la mise à jour sur place comme stratégie d'implémentation pour un langage conceptuellement pur, en empêchant le programme de conserver la valeur précédente (la mémoire qui serait mutée). C'est moins général que les structures de données persistantes: par exemple, vous ne pouvez pas facilement créer un journal d'annulation en stockant toutes les versions précédentes de l'état. C'est toujours un outil puissant, bien qu'AFAIK ne soit pas encore disponible dans les principaux langages fonctionnels.

Une autre option pour introduire en toute sécurité un état mutable dans un cadre fonctionnel est la STmonade de Haskell. Il peut être implémenté sans mutation et sans unsafe*fonctions, il se comporte comme s'il s'agissait simplement d'un habillage sophistiqué autour du passage implicite d'une structure de données persistante (cf. State). Mais en raison de la ruse du système qui impose l'ordre d'évaluation et empêche l'échappement, il peut être implémenté en toute sécurité avec une mutation sur place, avec tous les avantages de performance.

Communauté
la source
Il pourrait également être utile de mentionner les fermetures à glissière vous donnant la possibilité d'effectuer des changements rapides sur un focus dans une liste ou un arbre
jk.
1
@jk. Ils sont mentionnés dans le post sur l' informatique théorique auquel je suis lié. De plus, ils ne sont qu'une (enfin, une classe) de nombreuses structures de données pertinentes et leur discussion est hors de portée et peu utile.
assez juste, n'avait pas suivi les liens
jk.
9

Une structure mutable bon marché est la pile d'arguments.

Jetez un œil au calcul factoriel type SICP:

(defn fac (n accum) 
    (if (= n 1) 
        accum 
        (fac (- n 1) (* accum n)))

(defn factorial (n) (fac n 1))

Comme vous pouvez le voir, le deuxième argument de facest utilisé comme un accumulateur mutable pour contenir le produit à évolution rapide n * (n-1) * (n-2) * .... Cependant, aucune variable modifiable n'est en vue et il n'y a aucun moyen de modifier accidentellement l'accumulateur, par exemple à partir d'un autre thread.

Ceci est, bien sûr, un exemple limité.

Vous pouvez obtenir des listes liées immuables avec un remplacement bon marché du nœud principal (et par extension toute partie commençant par la tête): vous faites simplement pointer la nouvelle tête vers le même nœud suivant que l'ancienne tête. Cela fonctionne bien avec de nombreux algorithmes de traitement de liste ( foldbasés sur n'importe quoi ).

Vous pouvez obtenir de très bonnes performances à partir de tableaux associatifs basés, par exemple, sur des HAMT . Logiquement, vous recevez un nouveau tableau associatif avec des paires clé-valeur modifiées. L'implémentation peut partager la plupart des données communes entre les anciens et les nouveaux objets créés. Ce n'est cependant pas O (1); généralement, vous obtenez quelque chose de logarithmique, au moins dans le pire des cas. Les arbres immuables, en revanche, ne souffrent généralement pas de pénalité de performance par rapport aux arbres mutables. Bien sûr, cela nécessite une surcharge de mémoire, généralement loin d'être prohibitive.

Une autre approche est basée sur l'idée que si un arbre tombe dans une forêt et que personne ne l'entend, il n'a pas besoin de produire de son. Autrement dit, si vous pouvez prouver qu'un peu d'état muté ne quitte jamais une portée locale, vous pouvez muter les données qu'il contient en toute sécurité.

Clojure a des transitoires qui sont des ombres mutables de structures de données immuables qui ne fuient pas en dehors de la portée locale. Clean utilise Uniques pour obtenir quelque chose de similaire (si je me souviens bien). La rouille aide à faire des choses similaires avec des pointeurs uniques vérifiés statiquement.

9000
la source
1
+1, également pour avoir mentionné des types uniques dans Clean.
Giorgio
@ 9000 Je pense avoir entendu dire que Haskell a quelque chose de similaire aux transitoires de Clojure - quelqu'un me corrige si je me trompe.
paul
@paul: J'ai une connaissance très sommaire de Haskell, donc si vous pouviez fournir mes informations (au moins un mot-clé à google), j'inclurais volontiers une référence à la réponse.
9000
1
@paul, je n'en suis pas si sûr. Mais Haskell a une méthode pour créer quelque chose de similaire aux ML refet les limiter dans une certaine portée. Voir IORefou STRef. Et puis bien sûr il y a des TVars et des MVars qui sont similaires mais avec une sémantique concurrente saine (stm pour TVars et mutex basé sur MVars)
Daniel Gratzer
2

Ce que vous demandez est un peu trop large. O (1) retrait et remplacement de quelle position? Le chef d'une séquence? La queue? Une position arbitraire? La structure de données à utiliser dépend de ces détails. Cela dit, 2-3 Finger Trees semblent être l'une des structures de données persistantes les plus polyvalentes:

Nous présentons 2-3 arbres à doigts, une représentation fonctionnelle de séquences persistantes supportant l'accès aux extrémités en temps constant amorti, et la concaténation et la division en temps logarithmique de la taille de la plus petite pièce.

(...)

De plus, en définissant l'opération de division sous une forme générale, nous obtenons une structure de données à usage général qui peut servir de séquence, de file d'attente prioritaire, d'arbre de recherche, de file d'attente de recherche prioritaire et plus encore.

Les structures de données généralement persistantes ont des performances logarithmiques lors de la modification de positions arbitraires. Cela peut ou non être un problème, car la constante dans un algorithme O (1) peut être élevée, et le ralentissement logarithmique peut être "absorbé" dans un algorithme global plus lent.

Plus important encore, les structures de données persistantes facilitent le raisonnement sur votre programme, et cela devrait toujours être votre mode de fonctionnement par défaut. Vous devez privilégier les structures de données persistantes dans la mesure du possible et n'utiliser une structure de données modifiables qu'une fois que vous avez profilé et déterminé que la structure de données persistante est un goulot d'étranglement des performances. Tout le reste est une optimisation prématurée.

Doval
la source