web-dev-qa-db-fra.com

Haskell: Listes, Tableaux, Vecteurs, Séquences

J'apprends Haskell et lis quelques articles sur les différences de performances entre les listes Haskell et les tableaux de (insérez votre langue).

En tant qu'apprenant, je n'utilise évidemment que des listes sans même penser à la différence de performance. J'ai récemment commencé à étudier et trouvé de nombreuses bibliothèques de structures de données disponibles dans Haskell.

Quelqu'un peut-il s'il vous plaît expliquer la différence entre Listes, Tableaux, Vecteurs, Séquences sans aller très loin dans la théorie informatique des structures de données?

En outre, existe-t-il des modèles courants dans lesquels vous utiliseriez une structure de données au lieu d'une autre?

Y a-t-il d'autres formes de structures de données qui me manquent et qui pourraient être utiles?

217
r.sendecky

Listes Rock

De loin la structure de données la plus conviviale pour les données séquentielles dans Haskell est la liste

 data [a] = a:[a] | []

Les listes vous donnent cons (1) contre et correspondance de motif. La bibliothèque standard, et d'ailleurs le prélude, regorge de fonctions de liste utiles qui devraient filtrer votre code (foldr, map, filter). Les listes sont persistantes , autrement dit purement fonctionnelles, ce qui est très agréable. Les listes Haskell ne sont pas vraiment des "listes" car elles sont coinductives (d'autres langages appellent ces flux).

ones :: [Integer]
ones = 1:ones

twos = map (+1) ones

tenTwos = take 10 twos

travailler à merveille. Les structures de données infinies basculent.

Les listes en Haskell fournissent une interface un peu comme les itérateurs dans les langages impératifs (à cause de la paresse). Il est donc logique qu’ils soient largement utilisés.

D'autre part

Le premier problème avec les listes est que l'indexation dans celles-ci (!!) Prend (k) temps, ce qui est agaçant. De plus, les ajouts peuvent être lents ++, Mais le modèle d'évaluation paresseux de Haskell signifie qu'ils peuvent être traités comme s'ils étaient totalement amortis.

Le deuxième problème avec les listes est qu’elles ont une localisation de données médiocre. Les vrais processeurs ont des constantes élevées lorsque les objets en mémoire ne sont pas disposés les uns à côté des autres. Donc, en C++, std::vector A un "snoc" plus rapide (que mettre des objets à la fin) que n’importe quelle structure de données de liste chaînée pure que je connaisse, bien que ce ne soit pas une structure de données persistante, donc moins conviviale que les listes de Haskell.

Le troisième problème des listes est qu’elles occupent peu d’espace. Bouquets de pointeurs supplémentaires Augmentez votre capacité de stockage (par un facteur constant).

Les séquences sont fonctionnelles

Data.Sequence Est basé en interne sur doigtiers (je sais, vous ne voulez pas le savoir), ce qui signifie qu'ils ont quelques propriétés intéressantes

  1. Purement fonctionnel. Data.Sequence Est une structure de données totalement persistante.
  2. Darn un accès rapide au début et à la fin de l'arbre. ϴ (1) (amorti) pour obtenir le premier ou le dernier élément, ou pour ajouter des arbres. Au moins les listes de choses sont les plus rapides, Data.Sequence Est au plus constant.
  3. ϴ (log n) accès au milieu de la séquence. Cela inclut l’insertion de valeurs pour créer de nouvelles séquences
  4. API de haute qualité

D'autre part, Data.Sequence Ne fait pas grand chose pour le problème de la localité de données et ne fonctionne que pour les collections finies (c'est moins fainéant que les listes)

Les tableaux ne sont pas pour les âmes sensibles

Les tableaux constituent l'une des structures de données les plus importantes de CS, mais ils ne s'intègrent pas très bien dans le monde purement fonctionnel paresseux. Les tableaux fournissent ϴ (1) un accès au milieu de la collection et des facteurs de localisation/constante exceptionnellement bons. Mais, comme ils ne s'intègrent pas très bien dans Haskell, ils sont difficiles à utiliser. Il existe actuellement une multitude de types de tableaux différents dans la bibliothèque standard actuelle. Celles-ci incluent des tableaux entièrement persistants, des tableaux mutables pour le IO monad, des tableaux mutables pour le monad ST et des versions non encadrées de ce qui précède. Pour plus de précisions, le wiki haskell

Le vecteur est un "meilleur" tableau

Le paquet Data.Vector Fournit toutes les qualités du tableau, dans une API de niveau supérieur et plus propre. Sauf si vous savez vraiment ce que vous faites, vous devriez les utiliser si vous avez besoin de performances similaires à celles d'un tableau. Bien sûr, certaines mises en garde continuent de s'appliquer - les tableaux mutables comme les structures de données ne jouent tout simplement pas à Nice dans des langages paresseux purs. Pourtant, parfois, vous voulez que la performance O(1), et Data.Vector Vous le donne dans un paquet utilisable.

Vous avez d'autres options

Si vous voulez juste des listes avec la possibilité d’insérer efficacement à la fin, vous pouvez utiliser une liste de différences . Le meilleur exemple de performance décoiffée des listes vient de [Char], Que le prélude alias alias String. Les listes Char sont pratiques, mais ont tendance à fonctionner 20 fois plus lentement que les chaînes C, alors n'hésitez pas à utiliser Data.Text ou très rapidement Data.ByteString. Je suis sûr qu'il y a d'autres bibliothèques orientées séquence que je ne pense pas pour le moment.

Conclusion

Plus de 90% du temps, j'ai besoin d'une collection séquentielle dans les listes Haskell qui constituent la bonne structure de données. Les listes sont comme des itérateurs, les fonctions qui en consomment peuvent être facilement utilisées avec n'importe laquelle de ces autres structures de données en utilisant les fonctions toList qu'ils contiennent. Dans un monde meilleur, le prélude serait entièrement paramétrique quant au type de conteneur utilisé, mais actuellement [] Lit la bibliothèque standard. Donc, utiliser des listes (presque) partout est vraiment correct.
Vous pouvez obtenir des versions entièrement paramétriques de la plupart des fonctions de la liste (et leur utilisation est noble)

Prelude.map                --->  Prelude.fmap (works for every Functor)
Prelude.foldr/foldl/etc    --->  Data.Foldable.foldr/foldl/etc
Prelude.sequence           --->  Data.Traversable.sequence
etc

En fait, Data.Traversable Définit une API qui est plus ou moins universelle dans toute "liste comme".

Pourtant, bien que vous puissiez être bon et n’écrire que du code entièrement paramétrique, la plupart d’entre nous ne le sommes pas et utilisons la liste partout. Si vous apprenez, je vous suggère fortement de le faire aussi.


EDIT: D'après les commentaires, je me rends compte que je n'ai jamais expliqué quand utiliser Data.Vector Vs Data.Sequence. Les tableaux et les vecteurs fournissent des opérations d’indexation et de découpage extrêmement rapides, mais sont fondamentalement des structures de données transitoires (impératives). Des structures de données fonctionnelles pures telles que Data.Sequence Et [] Permettent de produire efficacement de nouvelles valeurs à partir d'anciennes valeurs comme si vous aviez modifié le anciennes valeurs.

  newList oldList = 7 : drop 5 oldList

ne modifie pas l'ancienne liste, et il n'est pas obligé de la copier. Donc, même si oldList est incroyablement long, cette "modification" sera très rapide. De même

  newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence 

produira une nouvelle séquence avec un newValue à la place de son élément 3000. Encore une fois, cela ne détruit pas l’ancienne séquence, mais en crée une nouvelle. Mais il le fait très efficacement, en prenant O (log (min (k, k-n))) où n est la longueur de la séquence et k l’indice que vous modifiez.

Vous ne pouvez pas le faire facilement avec Vectors et Arrays. Ils peuvent être modifiés mais il s’agit d’une modification impérative réelle, qui ne peut donc pas être effectuée dans le code Haskell normal. Cela signifie que les opérations dans le package Vector qui apportent des modifications telles que snoc et cons doivent copier tout le vecteur, prenez donc O(n) temps. La seule exception à cette règle est que vous pouvez utiliser la version mutable (Vector.Mutable) Dans le ST monad (ou IO) et faire toutes vos modifications comme vous le feriez dans un langage impératif. Lorsque vous avez terminé, vous "geler" votre vecteur pour qu'il devienne la structure immuable que vous souhaitez utiliser avec du code pur.

Mon sentiment est que vous devriez utiliser par défaut Data.Sequence Si une liste n'est pas appropriée. Utilisez Data.Vector Uniquement si votre modèle d'utilisation ne nécessite pas beaucoup de modifications ou si vous avez besoin de performances extrêmement élevées dans les monades ST/IO.

Si tout ce discours sur la monade ST vous laisse confus: une raison de plus de rester fidèle à la pure et belle Data.Sequence.

325
Philip JF