Ich entwickle eine GUI-Anwendung, die stark mit Grafiken arbeitet - Sie können sich das Beispiel als Vektoreditor vorstellen. Es ist sehr verlockend, alle Datenstrukturen unveränderlich zu machen - so konnte ich fast ohne Aufwand rückgängig machen / wiederholen, kopieren / einfügen und viele andere Dinge.
Der Einfachheit halber werde ich das folgende Beispiel verwenden: Die Anwendung wird zum Bearbeiten von polygonalen Formen verwendet, daher habe ich das Objekt "Polygon", bei dem es sich lediglich um eine Liste unveränderlicher Punkte handelt:
Scene -> Polygon -> Point
Daher habe ich nur eine veränderbare Variable in meinem Programm - die, die das aktuelle Szenenobjekt enthält. Das Problem, das ich habe, beginnt, wenn ich versuche, das Ziehen von Punkten zu implementieren. In einer veränderlichen Version greife ich einfach nach einem Point
Objekt und beginne, seine Koordinaten zu ändern. In unveränderlicher Version - ich stecke fest. Ich hätte Indizes von Polygon
aktuell Scene
, Index von gezogenem Punkt in speichern Polygon
und jedes Mal ersetzen können. Dieser Ansatz lässt sich jedoch nicht skalieren. Wenn die Zusammensetzung auf 5 und höher steigt, wird die Kesselplatte unerträglich.
Ich bin sicher, dass dieses Problem gelöst werden kann - schließlich gibt es Haskell mit völlig unveränderlichen Strukturen und IO-Monade. Aber ich kann einfach nicht finden wie.
Kannst du mir einen Hinweis geben?
quelle
Antworten:
Sie haben absolut Recht, dieser Ansatz lässt sich nicht skalieren, wenn Sie die Kesselplatte nicht umgehen können . Insbesondere wurde die Boilerplate zum Erstellen einer ganz neuen Szene mit einem winzigen Teil geändert. Viele funktionale Sprachen bieten jedoch ein Konstrukt für den Umgang mit dieser Art der Manipulation verschachtelter Strukturen: Linsen.
Ein Objektiv ist im Grunde ein Getter und Setter für unveränderliche Daten. Ein Objektiv konzentriert sich auf einen kleinen Teil einer größeren Struktur. Bei einer Linse gibt es zwei Dinge , die man damit machen kann - man kann sieht den kleinen Teil eines Wertes der größeren Struktur, oder Sie können festlegen , den kleinen Teil eines Wertes einer größeren Struktur auf einen neuen Wert. Angenommen, Sie haben ein Objektiv, das sich auf das dritte Element in einer Liste konzentriert:
Dieser Typ bedeutet, dass die größere Struktur eine Liste von Dingen ist und der kleine Unterabschnitt eines dieser Dinge ist. Mit diesem Objektiv können Sie das dritte Element in der Liste anzeigen und festlegen:
Der Grund, warum Objektive nützlich sind, liegt darin, dass sie Werte darstellen, die Getter und Setter darstellen, und dass Sie sie genauso abstrahieren können wie andere Werte. Sie können Funktionen erstellen, die Objektive zurückgeben, z. B. eine
listItemLens
Funktion, die eine Zahln
akzeptiert und ein Objektiv zurückgibt, das dasn
dritte Element in einer Liste anzeigt. Zusätzlich können Linsen zusammengesetzt werden :Jedes Objektiv kapselt das Verhalten zum Durchlaufen einer Ebene der Datenstruktur. Indem Sie sie kombinieren, können Sie die Kesselplatte für das Durchqueren mehrerer Ebenen komplexer Strukturen entfernen. Angenommen, Sie haben eine
scenePolygonLens i
, die dasi
dritte Polygon in einer Szene anzeigt, und einepolygonPointLens n
, die dennth
Punkt in einem Polygon anzeigt, können Sie einen Linsenkonstruktor erstellen, um sich auf genau den Punkt zu konzentrieren, den Sie in einer gesamten Szene interessieren:Angenommen, ein Benutzer klickt auf Punkt 3 des Polygons 14 und verschiebt ihn um 10 Pixel nach rechts. Sie können Ihre Szene folgendermaßen aktualisieren:
Dies enthält die gesamte Boilerplate zum Durchlaufen und Aktualisieren einer Szene im Inneren
lens
. Alles, was Sie beachten müssen, ist, auf was Sie den Punkt ändern möchten. Sie können dies mit einerlensTransform
Funktion weiter abstrahieren , die ein Objektiv, ein Ziel und eine Funktion zum Aktualisieren der Ansicht des Ziels durch das Objektiv akzeptiert :Dies nimmt eine Funktion und verwandelt sie in einen "Updater" für eine komplizierte Datenstruktur, wobei die Funktion nur auf die Ansicht angewendet und zum Erstellen einer neuen Ansicht verwendet wird. Kehren wir also zu dem Szenario zurück, in dem der 3. Punkt des 14. Polygons um 10 Pixel nach rechts verschoben wird. Dies kann folgendermaßen ausgedrückt
lensTransform
werden:Und das ist alles, was Sie brauchen, um die gesamte Szene zu aktualisieren. Dies ist eine sehr leistungsstarke Idee und funktioniert sehr gut, wenn Sie einige nützliche Funktionen zum Erstellen von Objektiven haben, mit denen Sie die Daten anzeigen können, die Ihnen wichtig sind.
Allerdings ist dies derzeit alles ziemlich weit draußen, selbst in der funktionalen Programmier-Community. Es ist schwierig, eine gute Bibliotheksunterstützung für die Arbeit mit Objektiven zu finden, und noch schwieriger zu erklären, wie sie funktionieren und welche Vorteile sie für Ihre Mitarbeiter haben. Nehmen Sie diesen Ansatz mit einem Körnchen Salz.
quelle
Ich habe an genau dem gleichen Problem gearbeitet (aber nur mit 3 Kompositionsstufen). Die Grundidee ist, zu klonen und dann zu ändern . Im unveränderlichen Programmierstil müssen das Klonen und Ändern zusammen erfolgen, was zum Befehlsobjekt wird .
Beachten Sie, dass im veränderlichen Programmierstil das Klonen ohnehin notwendig gewesen wäre:
Im veränderlichen Programmierstil
Im unveränderlichen Programmierstil,
quelle
Tief unveränderliche Objekte haben den Vorteil, dass das tiefe Klonen von etwas lediglich das Kopieren einer Referenz erfordert. Sie haben den Nachteil, dass für eine kleine Änderung an einem tief verschachtelten Objekt eine neue Instanz jedes Objekts erstellt werden muss, in dem es verschachtelt ist. Veränderbare Objekte haben den Vorteil, dass das Ändern eines Objekts einfach ist - tun Sie es einfach -, aber das tiefe Klonen eines Objekts erfordert das Erstellen eines neuen Objekts, das einen tiefen Klon jedes verschachtelten Objekts enthält. Schlimmer noch, wenn man will , ein Objekt klonen und eine Änderung vornehmen, Klon , dass das Objekt, eine weitere Änderung vornehmen, usw. dann , egal wie groß oder klein die Veränderungen sind ein bis hat zu halten für jede gespeicherte Version der eine Kopie der gesamten Hierarchie Objektzustand. Böse.
Ein Ansatz, der in Betracht gezogen werden könnte, wäre die Definition eines abstrakten "vielleicht veränderlichen" Typs mit veränderlichen und tief unveränderlichen abgeleiteten Typen. Alle diese Typen würden eine
AsImmutable
Methode aufweisen; Wenn Sie diese Methode für eine tief unveränderliche Instanz eines Objekts aufrufen, wird diese Instanz einfach zurückgegeben. Das Aufrufen einer veränderlichen Instanz würde eine tief unveränderliche Instanz zurückgeben, deren Eigenschaften tief unveränderliche Schnappschüsse ihrer Entsprechungen im Original waren. Unveränderliche Typen mit veränderlichen Äquivalenten würden eineAsMutable
Methode verwenden, die eine veränderbare Instanz konstruieren würde, deren Eigenschaften mit denen des Originals übereinstimmen.Das Ändern eines verschachtelten Objekts in einem tief unveränderlichen Objekt würde erfordern, dass zuerst das äußere unveränderliche Objekt durch ein veränderliches Objekt ersetzt wird, dann die Eigenschaft, die das zu ändernde Objekt enthält, durch ein veränderliches Objekt usw. ersetzt wird, aber wiederholt Änderungen an demselben Aspekt des Objekts vorgenommen werden Für das Gesamtobjekt müssten keine zusätzlichen Objekte erstellt werden, bis versucht wird,
AsImmutable
ein veränderliches Objekt aufzurufen (wodurch die veränderlichen Objekte veränderlich bleiben, aber unveränderliche Objekte mit denselben Daten zurückgegeben werden).Als einfache, aber signifikante Optimierungen könnte jedes veränderbare Objekt einen zwischengespeicherten Verweis auf ein Objekt seines zugeordneten unveränderlichen Typs enthalten, und jeder unveränderliche Typ sollte seinen
GetHashCode
Wert zwischenspeichern.AsImmutable
Überprüfen Sie beim Aufrufen eines veränderlichen Objekts, bevor Sie ein neues unveränderliches Objekt zurückgeben, dass es mit der zwischengespeicherten Referenz übereinstimmt. Wenn ja, geben Sie die zwischengespeicherte Referenz zurück (wobei Sie das neue unveränderliche Objekt verlassen). Aktualisieren Sie andernfalls die zwischengespeicherte Referenz, um das neue Objekt aufzunehmen, und geben Sie es zurück. In diesem Fall wiederholte Anrufe anAsImmutable
ohne dazwischenliegende Mutationen ergeben sich die gleichen Objektreferenzen. Selbst wenn man die Kosten für die Erstellung der neuen Instanzen nicht spart, vermeidet man die Speicherkosten für deren Aufbewahrung. Ferner können Gleichheitsvergleiche zwischen den unveränderlichen Objekten stark beschleunigt werden, wenn in den meisten Fällen die verglichenen Elemente referenzgleich sind oder unterschiedliche Hash-Codes aufweisen.quelle