Il y a quelques exemples intéressants ici, mais je voulais intervenir avec des exemples personnels où l'immutabilité a aidé énormément. Dans mon cas, j'ai commencé par concevoir une structure de données concurrente immuable, principalement dans l'espoir de pouvoir exécuter du code en toute confiance en parallèle avec des lectures et des écritures qui se chevauchent, sans avoir à se soucier des conditions de concurrence. Il y a eu une discussion que John Carmack a donnée m'a inspiré à le faire lorsqu'il a parlé d'une telle idée. C'est une structure assez basique et triviale à implémenter comme ceci:
Bien sûr, avec quelques cloches et des sifflets de plus, c'est comme être capable de supprimer des éléments en temps constant et de laisser des trous récupérables derrière et de laisser les blocs se dérégler s'ils deviennent vides et potentiellement libérés pour une instance immuable donnée. Mais fondamentalement, pour modifier la structure, vous modifiez une version "transitoire" et vous engagez de manière atomique les modifications que vous y avez apportées pour obtenir une nouvelle copie immuable qui ne touche pas l'ancienne, la nouvelle version ne créant que de nouvelles copies des blocs doivent être rendus uniques tout en faisant une copie superficielle et en comptant les autres.
Cependant, je ne trouve pas queutile pour le multithreading. Après tout, il y a toujours le problème conceptuel selon lequel, par exemple, un système physique applique la physique simultanément alors qu'un joueur essaie de déplacer des éléments dans un monde. Avec quelle copie immuable des données transformées allez-vous, celle que le joueur a transformée ou celle que le système physique a transformée? Donc, je n'ai pas vraiment trouvé de solution simple et agréable à ce problème conceptuel de base, si ce n'est d'avoir des structures de données modifiables qui se verrouillent de manière plus intelligente et découragent les lectures et les écritures qui se chevauchent dans les mêmes sections du tampon pour éviter les problèmes de threads. C'est quelque chose que John Carmack semble avoir probablement compris comment résoudre ses problèmes. au moins, il en parle comme s'il pouvait presque trouver une solution sans ouvrir une voiture de vers. Je ne suis pas allé aussi loin que lui à cet égard. Tout ce que je peux voir, ce sont des questions de conception sans fin si j'essayais de tout mettre en parallèle avec Immutables. J'aimerais pouvoir passer une journée à le cueillir dans la tête, car la plupart de mes efforts ont débuté avec ces idées qu'il a proposées.
Néanmoins, j'ai trouvé une valeur énorme de cette structure de données immuable dans d'autres domaines. Je l'utilise même maintenant pour stocker des images vraiment bizarres et rendant l'accès aléatoire nécessitant quelques instructions supplémentaires (décalage droit et binaire and
avec couche de pointeur indirectionnel), mais je traiterai des avantages ci-dessous.
Annuler le système
L'un des endroits les plus immédiats dont j'ai tiré profit était le système d'annulation. Le code de système d'annulation était l'une des choses les plus sujettes aux erreurs de ma région (secteur des effets visuels), et pas seulement dans les produits sur lesquels j'ai travaillé, mais aussi dans les produits concurrents (leurs systèmes d'annulation étaient également irréguliers) car il y avait tellement de solutions différentes. types de données à craindre d'annuler et de refaire correctement (système de propriétés, changements de données de maillage, changements de shader qui n'étaient pas basés sur des propriétés, comme l'échange entre eux, changements de hiérarchie de scènes comme changer le parent d'un enfant, changements d'image / texture, etc. etc. etc.).
Ainsi, la quantité de code d'annulation nécessaire était énorme, rivalisant souvent avec la quantité de code implémentant le système pour lequel le système d'annulation devait enregistrer les changements d'état. En m'appuyant sur cette structure de données, j'ai pu réduire le système d'annulation à ceci:
on user operation:
copy entire application state to undo entry
perform operation
on undo/redo:
swap application state with undo entry
Normalement, le code ci-dessus serait extrêmement inefficace lorsque vos données de scène couvrent des gigaoctets pour le copier intégralement. Mais cette structure de données ne copie que de manière superficielle les choses qui n'ont pas changé, et elle permet en fait de stocker, de manière peu coûteuse, une copie immuable de l'état complet de l'application. Je peux donc maintenant mettre en œuvre des systèmes d'annulation aussi facilement que le code ci-dessus et me concentrer sur l'utilisation de cette structure de données immuable pour rendre la copie des parties non modifiées de l'état de l'application moins chère, moins chère et moins chère. Depuis que j'ai commencé à utiliser cette structure de données, tous mes projets personnels ont des systèmes d'annulation utilisant uniquement ce modèle simple.
Maintenant, il y a encore des frais généraux ici. La dernière fois que j’ai mesuré que c’était environ 10 kilo-octets, il suffit de copier superficiellement l’état complet de l’application sans y apporter de modification (ceci est indépendant de la complexité de la scène car celle-ci est organisée dans une hiérarchie. Ainsi, rien ne change en dessous de la racine, est copié peu profond sans avoir à descendre dans les enfants). C’est loin de 0 octets, ce qui serait nécessaire pour un système d'annulation stockant uniquement des deltas. Toutefois, avec 10 kilo-octets de temps supplémentaire d'annulation par opération, il ne reste qu'un mégaoctet pour 100 opérations utilisateur. De plus, je pourrais encore potentiellement le réduire davantage si nécessaire.
Sécurité d'exception
La sécurité des exceptions avec une application complexe n’est pas une mince affaire. Cependant, lorsque l'état de votre application est immuable et que vous utilisez uniquement des objets transitoires pour tenter de valider des transactions de changement atomique, il est intrinsèquement protégé contre les exceptions, car si une partie du code est jetée, le composant transitoire est éliminé avant de donner une nouvelle copie immuable. . Cela banalise donc l’une des choses les plus difficiles que j’ai toujours trouvées à réussir dans une base de code C ++ complexe.
Trop de gens utilisent souvent simplement des ressources conformes à RAII en C ++ et pensent que cela suffit pour être à l'abri des exceptions. Ce n’est souvent pas le cas, puisqu’une fonction peut généralement causer des effets secondaires aux États situés au-delà de ceux situés localement. Dans ces cas, vous devez généralement commencer à traiter avec des gardes-objectifs et une logique de retour en arrière sophistiquée. Cette structure de données a été conçue pour que je n'ai souvent pas à m'en préoccuper, car les fonctions ne causent pas d'effets secondaires. Ils renvoient des copies immuables transformées de l'état de l'application au lieu de transformer l'état de l'application.
Montage non destructif
L’édition non destructive consiste essentiellement à superposer, empiler et relier des opérations sans toucher aux données de l’utilisateur original (il suffit de saisir des données et d’en sortir des données sans toucher à l’entrée). Il est généralement trivial de le mettre en œuvre avec une simple application d’image, telle que Photoshop, et cette structure de données ne bénéficiera peut-être pas beaucoup de cette opération, car de nombreuses opérations pourraient vouloir transformer chaque pixel de l’ensemble de l’image.
Cependant, avec l'édition de maillage non destructif, par exemple, de nombreuses opérations ne veulent souvent transformer qu'une partie du maillage. Une opération peut simplement vouloir déplacer des sommets ici. Un autre pourrait simplement vouloir subdiviser certains polygones à cet endroit. Ici, la structure de données immuable aide beaucoup à éviter la nécessité de faire une copie complète de tout le maillage pour renvoyer une nouvelle version du maillage avec une petite partie de celle-ci modifiée.
Minimiser les effets secondaires
Avec ces structures en main, il est également facile d’écrire des fonctions qui minimisent les effets secondaires sans encourir d’énormes pertes de performances. Je me suis retrouvé à écrire de plus en plus de fonctions qui ne font que restituer des structures de données immuables par valeur ces derniers temps sans que cela n'entraîne d'effets secondaires, même lorsque cela semble un peu inutile.
Par exemple, la tentation de transformer un groupe de positions consiste généralement à accepter une matrice et une liste d'objets et à les transformer de manière mutable. Ces jours-ci, je me retrouve à renvoyer une nouvelle liste d'objets.
Lorsque votre système contient davantage de fonctions comme celle-ci ne causant pas d'effets secondaires, il est nettement plus facile de raisonner au sujet de son exactitude, ainsi que de le tester.
Les avantages des copies bon marché
Donc de toute façon, ce sont les domaines dans lesquels j'ai trouvé le plus d'utilisation de structures de données immuables (ou de structures de données persistantes). Au départ, j’ai eu un peu trop de zèle et j’ai créé un arbre immuable, une liste chaînée immuable et une table de hachage immuable, mais au fil du temps, j’ai rarement trouvé autant d’utilisation pour ceux-ci. Dans le diagramme ci-dessus, j'ai principalement trouvé l'utilisation la plus fréquente du conteneur en forme de tableau immuable et volumineux.
Il me reste également beaucoup de code travaillant avec des mutables (la pratique le trouve moins nécessaire pour le code de bas niveau), mais l’état principal de l’application est une hiérarchie immuable, descendant d’une scène immuable à des composants immuables qui la composent. Certains des composants les moins chers sont toujours intégralement copiés, mais les plus chers, tels que les maillages et les images, utilisent la structure immuable pour autoriser les copies partielles peu coûteuses des seules pièces à transformer.
ConcurrentModificationException
qui est généralement causé par le même thread qui mute la collection dans le même thread, dans le corps d'uneforeach
boucle sur la même collection.