Strutture immutabili e gerarchia della composizione profonda


9

Sto sviluppando un'applicazione GUI, lavorando pesantemente con la grafica: puoi pensarci come un editor vettoriale, per il bene dell'esempio. È molto allettante rendere immutabili tutte le strutture di dati, in modo da poter annullare / ripetere, copiare / incollare e molte altre cose quasi senza sforzo.

Per semplicità, userò il seguente esempio: l'applicazione viene utilizzata per modificare forme poligonali, quindi ho l'oggetto "Poligono", che è semplicemente un elenco di punti immutabili:

Scene -> Polygon -> Point

E quindi ho solo una variabile mutabile nel mio programma - quella che contiene l'oggetto Scene corrente. Il problema che ho si presenta quando provo a implementare il trascinamento dei punti: nella versione mutabile, prendo semplicemente un Pointoggetto e inizio a modificarne le coordinate. Nella versione immutabile, sono bloccato. Avrei potuto memorizzare gli indici di Polygoncorrente Scene, l'indice del punto trascinato Polygone sostituirli ogni volta. Ma questo approccio non si ridimensiona: quando i livelli di composizione vanno a 5 e oltre, la piastra di cottura diventa insopportabile.

Sono sicuro che questo problema può essere risolto - dopo tutto, c'è Haskell con strutture completamente immutabili e monade IO. Ma non riesco proprio a trovare come.

Puoi darmi un suggerimento?


@Job - è così che funziona in questo momento, e mi dà molti dolori. Quindi sto cercando approcci alternativi - e l'immutabilità sembra perfetta per questa struttura applicativa, almeno prima di aggiungere l'interazione dell'utente :)
Rogach,

@Rogach: puoi spiegarci di più sul codice della tua caldaia?
rwong

Risposte:


9

Avrei potuto memorizzare gli indici di poligono nella scena corrente, l'indice del punto trascinato in poligono e sostituirlo ogni volta. Ma questo approccio non si ridimensiona - quando i livelli di composizione vanno a 5 e oltre, la piastra di cottura diventerebbe insopportabile.

Hai assolutamente ragione, questo approccio non si ridimensiona se non riesci a aggirare la piastra della caldaia . In particolare, la piastra di caldaia per la creazione di una scena completamente nuova con un piccolo sottoparte è cambiata. Tuttavia, molti linguaggi funzionali forniscono un costrutto per affrontare questo tipo di manipolazione della struttura nidificata: le lenti.

Un obiettivo è fondamentalmente un getter e setter per dati immutabili. Un obiettivo è focalizzato su una piccola parte di una struttura più grande. Data una lente, ci sono due cose che puoi fare con essa: puoi visualizzare la piccola parte di un valore della struttura più grande oppure puoi impostare la piccola parte di un valore di una struttura più grande su un nuovo valore. Ad esempio, supponiamo di avere un obiettivo focalizzato sul terzo elemento in un elenco:

thirdItemLens :: Lens [a] a

Questo tipo significa che la struttura più grande è un elenco di cose e la sottoparte piccola è una di quelle cose. Dato questo obiettivo, è possibile visualizzare e impostare il terzo elemento nell'elenco:

> view thirdItemLens [1, 2, 3, 4, 5]
3
> set thirdItemLens 100 [1, 2, 3, 4, 5]
[1, 2, 100, 4, 5]

Il motivo per cui gli obiettivi sono utili è perché sono valori che rappresentano getter e setter e puoi astrarre su di essi nello stesso modo in cui puoi fare altri valori. È possibile creare funzioni che restituiscono obiettivi, ad esempio una listItemLensfunzione che accetta un numero ne restituisce un obiettivo che visualizza l' nelemento th in un elenco. Inoltre, gli obiettivi possono essere composti :

> firstLens = listItemLens 0
> thirdLens = listItemLens 2
> firstOfThirdLens = lensCompose firstLens thirdLens
> view firstOfThirdLens [[1, 2], [3, 4], [5, 6], [7, 8]]
5
> set firstOfThirdLens 100 [[1, 2], [3, 4], [5, 6], [7, 8]]
[[1, 2], [3, 4], [100, 6], [7, 8]]

Ogni obiettivo incapsula il comportamento per attraversare un livello della struttura dei dati. Combinandoli, è possibile eliminare il boilerplate per attraversare più livelli di strutture complesse. Ad esempio, supponendo di avere un punto di scenePolygonLens ivisualizzazione del ipoligono in una scena e un punto di polygonPointLens nvista del nthpunto in un poligono, puoi creare un costruttore di obiettivi per concentrarti solo sul punto specifico a cui tieni in un'intera scena in questo modo:

scenePointLens i n = lensCompose (polygonPointLens n) (scenePolygonLens i)

Supponiamo ora che un utente faccia clic sul punto 3 del poligono 14 e lo sposta di 10 pixel a destra. Puoi aggiornare la tua scena in questo modo:

lens = scenePointLens 14 3
point = view lens currentScene
newPoint = movePoint 10 0 point
newScene = set lens newPoint currentScene

Questo contiene bene tutto il boilerplate per attraversare e aggiornare una scena all'interno lens, tutto ciò di cui ti devi preoccupare è quello a cui vuoi cambiare il punto. Puoi astrarre ulteriormente questo con una lensTransformfunzione che accetta un obiettivo, un obiettivo e una funzione per aggiornare la vista dell'obiettivo attraverso l'obiettivo:

lensTransform lens transformFunc target =
  current = view lens target
  new = transformFunc current
  set lens new target

Questo accetta una funzione e la trasforma in un "programma di aggiornamento" su una struttura di dati complicata, applicando la funzione solo alla vista e utilizzandola per costruire una nuova vista. Quindi tornando allo scenario di spostare il 3 ° punto del 14 ° poligono a 10 pixel a destra, che può essere espresso in questo lensTransformmodo:

lens = scenePointLens 14 3
moveRightTen point = movePoint 10 0 point
newScene = lensTransform lens moveRightTen currentScene

E questo è tutto ciò che serve per aggiornare l'intera scena. Questa è un'idea molto potente e funziona molto bene quando hai delle belle funzioni per costruire obiettivi che visualizzano i pezzi dei tuoi dati che ti interessano.

Tuttavia, questo è tutto roba abbastanza là fuori al momento, anche nella comunità di programmazione funzionale. È difficile trovare un buon supporto di libreria per lavorare con gli obiettivi, e ancora più difficile spiegare come funzionano e quali sono i vantaggi per i tuoi colleghi. Prendi questo approccio con un granello di sale.


Spiegazione eccellente! Ora capisco quali sono le lenti!
Vincent Lecrubier,

13

Ho lavorato esattamente sullo stesso problema (ma solo con 3 livelli di composizione). L'idea di base è clonare, quindi modificare . In uno stile di programmazione immutabile, la clonazione e la modifica devono avvenire insieme, diventando oggetto di comando .

Si noti che in uno stile di programmazione mutevole, la clonazione sarebbe stata comunque necessaria:

  • Per consentire Annulla / Ripeti
  • Potrebbe essere necessario che il sistema di visualizzazione visualizzi contemporaneamente il modello "prima della modifica" e "durante la modifica", sovrapposti (come linee fantasma), in modo che l'utente possa vedere le modifiche.

In uno stile di programmazione mutevole,

  • La struttura esistente è clonata in profondità
  • Le modifiche vengono apportate nella copia clonata
  • Si dice al motore di visualizzazione di rendere a colori la vecchia struttura in linee fantasma e la struttura clonata / modificata.

In immutabile stile di programmazione,

  • Ogni azione dell'utente che comporta la modifica dei dati è associata a una sequenza di "comandi".
  • Un oggetto comando incapsula esattamente quale modifica deve essere applicata e un riferimento alla struttura originale.
    • Nel mio caso, il mio oggetto comando ricorda solo l'indice dei punti che deve essere modificato e le nuove coordinate. (cioè molto leggero, poiché non sto seguendo rigorosamente lo stile immutabile.)
  • Quando viene eseguito un oggetto comando, crea una copia profonda modificata della struttura, rendendo permanente la modifica nella nuova copia.
  • Man mano che l'utente effettua più modifiche, verranno creati più oggetti comando.

1
Perché fare una copia profonda di una struttura di dati immutabile? Devi solo copiare la "spina dorsale" dei riferimenti dall'oggetto modificato alla radice e conservare i riferimenti alle parti rimanenti della struttura originale.
Ripristina Monica il

3

Gli oggetti profondamente immutabili hanno il vantaggio che la clonazione profonda di qualcosa richiede semplicemente la copia di un riferimento. Hanno lo svantaggio che apportare anche una piccola modifica a un oggetto profondamente nidificato richiede la costruzione di una nuova istanza di ogni oggetto all'interno del quale è nidificato. Gli oggetti mutabili hanno il vantaggio che cambiare un oggetto è facile - basta farlo - ma la clonazione profonda di un oggetto richiede la costruzione di un nuovo oggetto che contiene un clone profondo di ogni oggetto nidificato. Peggio ancora, se si desidera clonare un oggetto e apportare una modifica, clonare quell'oggetto, apportare un'altra modifica, ecc., Non importa quanto grandi o piccoli siano i cambiamenti, è necessario conservare una copia dell'intera gerarchia per ogni versione salvata del stato dell'oggetto. Cattiva.

Un approccio che potrebbe valere la pena di considerare sarebbe quello di definire un tipo "forseMutable" astratto con tipi di derivati ​​mutabili e profondamente immutabili. Tutti questi tipi avrebbero un AsImmutablemetodo; chiamare quel metodo su un'istanza profondamente immutabile di un oggetto restituirebbe semplicemente quell'istanza. Chiamarlo su un'istanza mutabile restituirebbe un'istanza profondamente immutabile le cui proprietà erano istantanee profondamente immutabili dei loro equivalenti nell'originale. I tipi immutabili con equivalenti mutabili sfoggerebbero un AsMutablemetodo, che costruirebbe un'istanza mutabile le cui proprietà corrispondessero a quelle dell'originale.

Cambiare un oggetto nidificato in un oggetto profondamente immutabile richiederebbe prima di sostituire l'oggetto immutabile esterno con uno mutabile, quindi sostituire la proprietà contenente l'oggetto da modificare con uno mutabile, ecc., Ma apportare ripetute modifiche allo stesso aspetto del l'oggetto complessivo non richiederebbe la realizzazione di oggetti aggiuntivi fino al momento in cui si è tentato di richiamare AsImmutableun oggetto mutabile (che lascerebbe gli oggetti mutabili mutabili, ma restituirebbe oggetti immutabili con gli stessi dati).

Come ottimizzazioni semplici ma significative, ogni oggetto mutabile potrebbe contenere un riferimento memorizzato nella cache di un oggetto del tipo immutabile associato e ogni tipo immutabile dovrebbe memorizzare nella cache il suo GetHashCodevalore. Quando si chiama AsImmutableun oggetto mutabile, prima di restituire un nuovo oggetto immutabile, verificare che corrisponda al riferimento memorizzato nella cache. In tal caso, restituisce il riferimento memorizzato nella cache (abbandonando il nuovo oggetto immutabile). Altrimenti aggiorna il riferimento memorizzato nella cache per contenere il nuovo oggetto e restituirlo. In tal caso, chiamate ripetute aAsImmutablesenza mutazioni intervenute si otterranno gli stessi riferimenti a oggetti. Anche se non si risparmia il costo di costruzione delle nuove istanze, si eviterà il costo di memoria di conservarle. Inoltre, i confronti di uguaglianza tra gli oggetti immutabili possono essere notevolmente accelerati se nella maggior parte dei casi gli oggetti confrontati sono uguali al riferimento o hanno codici hash diversi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.