Juste des types généralement immuables créés dans des langages qui ne tournent pas autour de l'immuabilité auront tendance à coûter plus de temps au développeur pour créer ainsi que potentiellement utiliser s'ils nécessitent un type d'objet "constructeur" pour exprimer les changements souhaités (cela ne signifie pas que l'ensemble le travail sera plus, mais il y a un coût initial dans ces cas). De plus, que le langage facilite ou non la création de types immuables, il aura tendance à toujours nécessiter un certain traitement et une surcharge de mémoire pour les types de données non triviaux.
Rendre les fonctions dépourvues d'effets secondaires
Si vous travaillez dans des langages qui ne tournent pas autour de l'immuabilité, je pense que l'approche pragmatique n'est pas de chercher à rendre immuable chaque type de données. Un état d'esprit potentiellement beaucoup plus productif qui vous offre plusieurs des mêmes avantages consiste à se concentrer sur la maximisation du nombre de fonctions dans votre système qui ne provoquent aucun effet secondaire .
À titre d'exemple simple, si vous avez une fonction qui provoque un effet secondaire comme celui-ci:
// Make 'x' the absolute value of itself.
void make_abs(int& x);
Ensuite, nous n'avons pas besoin d'un type de données entier immuable qui interdit aux opérateurs comme l'affectation post-initialisation de faire en sorte que cette fonction évite les effets secondaires. Nous pouvons simplement faire ceci:
// Returns the absolute value of 'x'.
int abs(int x);
Maintenant, la fonction ne joue pas avec x
ou quoi que ce soit en dehors de sa portée, et dans ce cas trivial, nous pourrions même avoir réduit certains cycles en évitant toute surcharge associée à l'indirection / aliasing. À tout le moins, la deuxième version ne devrait pas être plus coûteuse en termes de calcul que la première.
Choses qui coûtent cher à copier en entier
Bien sûr, la plupart des cas ne sont pas si simples si nous voulons éviter qu'une fonction ne provoque des effets secondaires. Un cas d'utilisation complexe dans le monde réel pourrait ressembler davantage à ceci:
// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);
À ce stade, le maillage peut nécessiter quelques centaines de mégaoctets de mémoire avec plus de cent mille polygones, encore plus de sommets et d'arêtes, de multiples textures, des cibles de morphing, etc. transform
fonctionner sans effets secondaires, comme ceci:
// Returns a new version of the mesh whose vertices been
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);
Et c'est dans ces cas où la copie de quelque chose dans son intégralité serait normalement une surcharge épique où j'ai trouvé utile de se transformer Mesh
en une structure de données persistante et un type immuable avec le "générateur" analogique pour en créer des versions modifiées afin qu'il peut simplement copier peu profondément et compter des pièces qui ne sont pas uniques. Tout cela dans le but de pouvoir écrire des fonctions de maillage sans effets secondaires.
Structures de données persistantes
Et dans ces cas où tout copier est si incroyablement cher, j'ai trouvé que l'effort de concevoir un immuable était Mesh
vraiment rentable même s'il avait un coût légèrement élevé à l'avance, car cela ne simplifiait pas seulement la sécurité des threads. Il a également simplifié l'édition non destructive (permettant à l'utilisateur de superposer les opérations de maillage sans modifier sa copie d'origine), les systèmes d'annulation (désormais, le système d'annulation peut simplement stocker une copie immuable du maillage avant les modifications apportées par une opération sans faire exploser la mémoire). utilisation), et la sécurité des exceptions (maintenant, si une exception se produit dans la fonction ci-dessus, la fonction n'a pas besoin de revenir en arrière et d'annuler tous ses effets secondaires car elle n'en a pas causé au début).
Je peux dire avec confiance dans ces cas que le temps nécessaire pour rendre immuables ces structures de données lourdes a permis d'économiser plus de temps qu'il n'en a coûté, car j'ai comparé les coûts de maintenance de ces nouvelles conceptions avec les anciens qui tournaient autour de la mutabilité et des fonctions provoquant des effets secondaires, et les anciennes conceptions mutables coûtaient beaucoup plus de temps et étaient beaucoup plus sujettes aux erreurs humaines, en particulier dans les domaines qui sont vraiment tentants pour les développeurs de les négliger pendant les périodes critiques, comme la sécurité d'exception.
Je pense donc que les types de données immuables sont vraiment payants dans ces cas, mais tout ne doit pas être rendu immuable afin de rendre la majorité des fonctions de votre système sans effets secondaires. Beaucoup de choses sont assez bon marché pour simplement copier en entier. De nombreuses applications du monde réel devront également provoquer des effets secondaires ici et là (tout au moins comme enregistrer un fichier), mais il existe généralement beaucoup plus de fonctions qui pourraient être dépourvues d'effets secondaires.
Le but d'avoir certains types de données immuables pour moi est de s'assurer que nous pouvons écrire le nombre maximum de fonctions pour être exempt d'effets secondaires sans encourir de surcharge épique sous la forme de copies massives de structures de données massives à gauche et à droite en totalité lorsque seules de petites portions d'entre eux doivent être modifiés. La présence de structures de données persistantes dans ces cas finit par devenir un détail d'optimisation pour nous permettre d'écrire nos fonctions sans effets secondaires sans payer un coût épique.
Frais généraux immuables
Maintenant, conceptuellement, les versions modifiables auront toujours un avantage en termes d'efficacité. Il y a toujours cette surcharge de calcul associée aux structures de données immuables. Mais je l'ai trouvé un échange digne dans les cas que j'ai décrits ci-dessus, et vous pouvez vous concentrer à rendre la surcharge suffisamment minime dans la nature. Je préfère ce type d'approche où l'exactitude devient facile et l'optimisation devient plus difficile que l'optimisation étant plus facile mais l'exactitude devenant plus difficile. Ce n'est pas aussi démoralisant d'avoir du code qui fonctionne parfaitement correctement, qui a besoin de quelques mises au point supplémentaires sur du code qui ne fonctionne pas correctement en premier lieu, quelle que soit la rapidité avec laquelle il obtient ses résultats incorrects.