Je pense que la plupart des problèmes de "fuites de rigueur" sont dus au fait que les gens n'ont pas un bon modèle conceptuel. Les Haskellers sans un bon modèle conceptuel ont tendance à avoir et à propager la superstition que plus strict est mieux. Cette intuition provient peut-être des résultats obtenus en jouant avec de petits exemples et des boucles serrées. Mais elle est incorrecte. Il est tout aussi important d'être paresseux aux bons moments que d'être strict aux bons moments.
Il existe deux camps de types de données, généralement appelés "données" et "codonnées". Il est essentiel de respecter les schémas de chacun d'eux.
- Les opérations qui produisent des "données" (Int, ByteString, ...) doivent être forcées près de l'endroit où elles se produisent. Si j'ajoute un nombre à un accumulateur, je fais attention à ce qu'il soit forcé avant d'en ajouter un autre. Une bonne compréhension de la paresse est très importante ici, en particulier sa nature conditionnelle (c'est-à-dire que les propositions de rigueur ne prennent pas la forme "
X
est évalué" mais plutôt " quand Y
est évalué, il en est de même pour X
").
- Les opérations qui produisent et consommer "codata" (listes la plupart du temps, arbres, la plupart des autres types récursifs) doivent le faire de manière incrémentielle. Habituellement, la transformation codata -> codata doit produire de l'information pour chaque bit d'information qu'elle consomme (modulo le saut de type
filter
). Un autre élément important pour codata est que vous l'utilisez de manière linéaire chaque fois que possible -- c'est-à-dire que vous utilisez la queue d'une liste exactement une fois ; vous utilisez chaque branche d'un arbre exactement une fois. Cela garantit que le GC peut collecter les morceaux au fur et à mesure qu'ils sont consommés.
Il faut faire preuve d'une attention particulière lorsque des données codées contiennent des données. Par exemple. iterate (+1) 0 !! 1000
finira par produire un thunk de taille 1000 avant de l'évaluer. Vous devez à nouveau penser à la rigueur conditionnelle -- la façon d'éviter ce cas est de s'assurer que quand un contre de la liste est consommé, l'ajout de son élément se produit. iterate
viole ceci, donc nous avons besoin d'une meilleure version.
iterate' :: (a -> a) -> a -> [a]
iterate' f x = x : (x `seq` iterate' f (f x))
Lorsque vous commencez à composer des choses, il devient bien sûr plus difficile de dire quand les mauvais cas se produisent. En général, il est difficile de créer des structures de données / fonctions efficaces qui fonctionnent aussi bien sur les données que sur les codonnées, et il est important de garder à l'esprit lequel est lequel (même dans un cadre polymorphe où ce n'est pas garanti, vous devriez avoir une idée en tête et essayer de la respecter).
Le partage est délicat, et je pense que je l'aborde surtout au cas par cas. Parce que c'est délicat, j'essaie de le garder localisé, en choisissant de ne pas exposer de grandes structures de données aux utilisateurs de modules en général. Cela peut généralement être fait en exposant des combinateurs pour les modules générant la chose en question, puis la produire et la consommer d'un seul coup (la transformation de la codensité sur les monades en est un exemple).
Mon objectif de conception est de faire en sorte que chaque fonction respecte les modèles de données/codonnées de mes types. J'y parviens généralement (même si cela demande parfois une réflexion approfondie - c'est devenu naturel avec les années), et j'ai rarement des problèmes de fuite lorsque j'y parviens. Mais je ne prétends pas que c'est facile -- cela demande de l'expérience avec les bibliothèques et les modèles canoniques du langage. Ces décisions ne sont pas prises isolément, et tout doit être parfait en même temps pour que cela fonctionne bien. Un instrument mal accordé peut ruiner l'ensemble du concert (c'est pourquoi l'"optimisation par perturbation aléatoire" ne fonctionne presque jamais pour ce type de problèmes).
Apfelmus's Invariants spatiaux L'article est utile pour développer davantage votre intuition de l'espace et des objets. Voir également le commentaire d'Edward Kmett ci-dessous.