Je suis en train de développer une application GUI, travaillant fortement avec des graphiques - vous pouvez y penser comme un éditeur de vecteur, pour le plaisir de l'exemple. Il est très tentant de rendre toutes les structures de données immuables - afin que je puisse annuler / refaire, copier / coller et bien d'autres choses presque sans effort.
Pour des raisons de simplicité, je vais utiliser l'exemple suivant - l'application est utilisée pour éditer des formes polygonales, j'ai donc un objet "Polygone", qui est simplement une liste de points immuables:
Scene -> Polygon -> Point
Et donc je n'ai qu'une seule variable mutable dans mon programme - celle qui contient l'objet Scene actuel. Le problème que je rencontre commence lorsque j'essaie d'implémenter le glissement de points - en version mutable, je prends simplement un Point
objet et commence à modifier ses coordonnées. En version immuable - je suis coincé. J'aurais pu stocker les index de Polygon
dans le courant Scene
, l'index du point glissé Polygon
et le remplacer à chaque fois. Mais cette approche n'est pas évolutive - lorsque les niveaux de composition atteignent 5 et plus, le passe-partout devient insupportable.
Je suis sûr que ce problème peut être résolu - après tout, il y a Haskell avec des structures complètement immuables et une monade IO. Mais je n'arrive pas à trouver comment.
Pouvez-vous me donner un indice?
la source
Réponses:
Vous avez absolument raison, cette approche n'est pas évolutive si vous ne pouvez pas contourner le passe-partout . Plus précisément, le passe-partout pour créer une toute nouvelle scène avec une minuscule sous-partie a changé. Cependant, de nombreux langages fonctionnels fournissent une construction pour gérer ce type de manipulation de structure imbriquée: les lentilles.
Un objectif est essentiellement un getter et un setter pour des données immuables. Un objectif se concentre sur une petite partie d'une plus grande structure. Étant donné un objectif, il y a deux choses que vous pouvez faire avec lui - vous pouvez afficher la petite partie d'une valeur de la structure plus grande, ou vous pouvez définir la petite partie d'une valeur d'une structure plus grande sur une nouvelle valeur. Par exemple, supposons que vous ayez un objectif qui se concentre sur le troisième élément d'une liste:
Ce type signifie que la plus grande structure est une liste de choses, et la petite sous-partie est l'une de ces choses. Compte tenu de cet objectif, vous pouvez afficher et définir le troisième élément de la liste:
La raison pour laquelle les lentilles sont utiles est qu'elles sont des valeurs représentant des getters et des setters, et que vous pouvez les abstraire de la même manière que vous pouvez utiliser d'autres valeurs. Vous pouvez créer des fonctions qui renvoient des objectifs, par exemple une
listItemLens
fonction qui prend un nombren
et renvoie un objectif affichant len
e élément dans une liste. De plus, les lentilles peuvent être composées :Chaque objectif résume le comportement pour traverser un niveau de la structure de données. En les combinant, vous pouvez éliminer le passe-partout pour traverser plusieurs niveaux de structures complexes. Par exemple, en supposant que vous ayez un
scenePolygonLens i
qui affiche lei
e polygone dans une scène et unpolygonPointLens n
qui affiche lenth
point dans un polygone, vous pouvez créer un constructeur d'objectif pour se concentrer uniquement sur le point spécifique qui vous intéresse dans une scène entière, comme ceci:Supposons maintenant qu'un utilisateur clique sur le point 3 du polygone 14 et le déplace de 10 pixels vers la droite. Vous pouvez mettre à jour votre scène comme suit:
Celui-ci contient joliment tout le passe-partout pour parcourir et mettre à jour une scène à l'intérieur
lens
, tout ce dont vous avez besoin, c'est de savoir ce que vous voulez changer de point. Vous pouvez en outre résumer cela avec unelensTransform
fonction qui accepte un objectif, une cible et une fonction pour mettre à jour la vue de la cible à travers l'objectif:Cela prend une fonction et la transforme en "mise à jour" sur une structure de données compliquée, en appliquant la fonction uniquement à la vue et en l'utilisant pour construire une nouvelle vue. Revenons donc au scénario de déplacement du 3e point du 14e polygone vers la droite de 10 pixels, qui peut être exprimé en termes de
lensTransform
ceci:Et c'est tout ce dont vous avez besoin pour mettre à jour toute la scène. C'est une idée très puissante et fonctionne très bien lorsque vous avez de belles fonctions pour construire des lentilles affichant les éléments de vos données qui vous intéressent.
Cependant, ce sont des trucs assez excentriques actuellement, même dans la communauté de programmation fonctionnelle. Il est difficile de trouver un bon support de bibliothèque pour travailler avec des objectifs, et encore plus difficile d'expliquer comment ils fonctionnent et quels sont les avantages pour vos collègues. Adoptez cette approche avec un grain de sel.
la source
J'ai travaillé exactement sur le même problème (mais seulement avec 3 niveaux de composition). L'idée de base est de cloner, puis de modifier . Dans un style de programmation immuable, le clonage et la modification doivent se produire ensemble, ce qui devient un objet de commande .
Notez que dans un style de programmation modifiable, le clonage aurait été nécessaire de toute façon:
Dans un style de programmation modifiable,
Dans un style de programmation immuable,
la source
Les objets profondément immuables ont l'avantage que le clonage profond de quelque chose nécessite simplement la copie d'une référence. Ils ont l'inconvénient que même une petite modification d'un objet profondément imbriqué nécessite la construction d'une nouvelle instance de chaque objet dans lequel il est imbriqué. Les objets mutables ont l'avantage que changer un objet est facile - il suffit de le faire - mais le clonage en profondeur d'un objet nécessite la construction d'un nouvel objet qui contient un clone profond de chaque objet imbriqué. Pire, si l'on veut cloner un objet et effectuer une modification, cloner cet objet, effectuer une autre modification, etc., peu importe la taille des modifications, il faut conserver une copie de la hiérarchie entière pour chaque version enregistrée de la l'état de l'objet. Méchant.
Une approche qui pourrait être utile serait de définir un type abstrait "peut-être mutable" avec des types dérivés mutables et profondément immuables. Tous ces types auraient une
AsImmutable
méthode; appeler cette méthode sur une instance profondément immuable d'un objet retournerait simplement cette instance. L'appeler sur une instance mutable retournerait une instance profondément immuable dont les propriétés étaient des instantanés profondément immuables de leurs équivalents dans l'original. Les types immuables avec des équivalents mutables arboreraient uneAsMutable
méthode, qui construirait une instance mutable dont les propriétés correspondraient à celles de l'original.Changer un objet imbriqué dans un objet profondément immuable nécessiterait d'abord de remplacer l'objet immuable externe par un objet mutable, puis de remplacer la propriété contenant la chose à changer par un objet mutable, etc., mais en apportant des modifications répétées au même aspect du l'objet global ne nécessiterait pas de créer des objets supplémentaires jusqu'à ce qu'une tentative ait été faite d'appeler
AsImmutable
un objet mutable (ce qui laisserait les objets mutables mutables, mais retournerait des objets immuables contenant les mêmes données).En tant qu'optimisations simples mais significatives, chaque objet mutable peut contenir une référence en cache à un objet de son type immuable associé, et chaque type immuable doit mettre en cache sa
GetHashCode
valeur. Lors de l'appelAsImmutable
à un objet mutable, avant de renvoyer un nouvel objet immuable, vérifiez qu'il correspondrait à la référence mise en cache. Si tel est le cas, renvoyez la référence mise en cache (abandon du nouvel objet immuable). Sinon, mettez à jour la référence mise en cache pour contenir le nouvel objet et renvoyez-le. Dans ce cas, des appels répétés àAsImmutable
sans aucune mutation intermédiaire donnera les mêmes références d'objet. Même si l'on n'économise pas le coût de construction des nouvelles instances, on évitera le coût de la mémoire de les conserver. En outre, les comparaisons d'égalité entre les objets immuables peuvent être considérablement accélérées si dans la plupart des cas les éléments comparés sont égaux en référence ou ont des codes de hachage différents.la source