Ci sono alcuni esempi interessanti qui, ma ho voluto entrare con alcuni personali in cui l'immutabilità ha aiutato moltissimo. Nel mio caso, ho iniziato a progettare una struttura di dati simultanea immutabile principalmente con la speranza di poter semplicemente eseguire il codice in modo sicuro in parallelo con letture e scritture sovrapposte e senza doversi preoccupare delle condizioni di gara. C'è stato un discorso in cui John Carmack mi ha ispirato a farlo quando ha parlato di una simile idea. È una struttura piuttosto semplice e piuttosto banale da implementare in questo modo:
Ovviamente con qualche altro campanello e fischietto ad esso come essere in grado di rimuovere elementi in tempo costante e lasciare dietro di sé buchi recuperabili e fare in modo che i blocchi vengano eliminati se si svuotano e potenzialmente si liberano per una data istanza immutabile. Ma fondamentalmente per modificare la struttura, si modifica una versione "transitoria" e si impegnano atomicamente le modifiche apportate ad essa per ottenere una nuova copia immutabile che non tocca quella vecchia, con la nuova versione che crea solo nuove copie dei blocchi che devono essere resi unici durante la copia superficiale e il conteggio degli altri.
Tuttavia, non ho trovato è cheutile per scopi di multithreading. Dopotutto, c'è ancora il problema concettuale in cui, per esempio, un sistema fisico applica la fisica contemporaneamente mentre un giocatore sta cercando di spostare elementi in un mondo. Con quale copia immutabile dei dati trasformati vai, quella trasformata dal giocatore o quella trasformata dal sistema fisico? Quindi non ho davvero trovato una soluzione piacevole e semplice a questo problema concettuale di base se non quello di avere strutture di dati mutabili che si bloccano in un modo più intelligente e scoraggiano le letture e le scritture sovrapposte nelle stesse sezioni del buffer per evitare di bloccare i thread. Questo è qualcosa che John Carmack sembra aver probabilmente capito come risolvere nei suoi giochi; almeno ne parla come se potesse quasi vedere una soluzione senza aprire un'auto di vermi. Non sono arrivato fino a lui in questo senso. Tutto quello che posso vedere sono infinite domande sul design se provassi a parallelizzare tutto intorno a immutabili. Vorrei poter passare una giornata a raccogliere il cervello dal momento che la maggior parte dei miei sforzi è iniziata con quelle idee che ha buttato fuori.
Tuttavia, ho trovato un valore enorme di questa struttura di dati immutabile in altre aree. Lo uso persino ora per archiviare immagini che sono davvero strane e che richiedono un accesso casuale richiedono più istruzioni (spostamento a destra e bit a bit and
con uno strato di indiretto del puntatore), ma tratterò i vantaggi di seguito.
Annulla sistema
Uno dei posti più immediati che ho trovato per trarne vantaggio è stato il sistema di annullamento. Il codice di sistema di annullamento era una delle cose più soggette a errori nella mia area (settore degli effetti visivi) e non solo nei prodotti su cui ho lavorato ma in prodotti concorrenti (anche i loro sistemi di annullamento erano traballanti) perché c'erano così tanti diversi tipi di dati di cui preoccuparsi per l'annullamento e la ripetizione corretta (sistema di proprietà, modifiche dei dati mesh, cambiamenti dello shader che non erano basati su proprietà come lo scambio l'uno con l'altro, la gerarchia delle scene cambia come la modifica del genitore di un bambino, i cambiamenti di immagine / trama, ecc. ecc. ecc.).
Quindi la quantità di codice di annullamento richiesta era enorme, spesso in concorrenza con la quantità di codice che implementava il sistema per il quale il sistema di annullamento doveva registrare i cambiamenti di stato. Appoggiandosi a questa struttura di dati, sono stato in grado di ottenere il sistema di annullamento fino a questo:
on user operation:
copy entire application state to undo entry
perform operation
on undo/redo:
swap application state with undo entry
Normalmente quel codice sopra sarebbe enormemente inefficiente quando i dati della scena si estendono su gigabyte per copiarlo interamente. Ma questa struttura di dati copia solo superficialmente cose che non sono state modificate, e in realtà lo ha reso abbastanza economico archiviare una copia immutabile dell'intero stato dell'applicazione. Quindi ora posso implementare i sistemi di annullamento con la stessa facilità del codice sopra e concentrarmi solo sull'utilizzo di questa struttura di dati immutabile per rendere la copia di parti invariate dello stato dell'applicazione sempre più economica. Da quando ho iniziato a utilizzare questa struttura di dati, tutti i miei progetti personali hanno sistemi di annullamento usando solo questo semplice schema.
Ora c'è ancora un po 'di spese generali qui. L'ultima volta che ho misurato erano circa 10 kilobyte solo per copiare superficialmente l'intero stato dell'applicazione senza apportare modifiche ad esso (questo è indipendente dalla complessità della scena poiché la scena è disposta in una gerarchia, quindi se nulla al di sotto della radice cambia, solo la radice è poco profondo copiato senza dover scendere nei bambini). È lontano da 0 byte come sarebbe necessario per un sistema di annullamento che memorizza solo i delta. Ma a 10 kilobyte di overhead di annullamento per operazione, è ancora solo un megabyte per 100 operazioni utente. Inoltre potrei ancora potenzialmente comprimerlo ulteriormente in futuro, se necessario.
Eccezione-sicurezza
La sicurezza delle eccezioni con un'applicazione complessa non è cosa da poco. Tuttavia, quando lo stato dell'applicazione è immutabile e si utilizzano solo oggetti transitori per tentare di eseguire il commit di transazioni di cambiamento atomico, è intrinsecamente sicuro dalle eccezioni poiché se viene lanciata una parte del codice, il transitorio viene gettato via prima di dare una nuova copia immutabile . In modo che banalizzi una delle cose più difficili che ho sempre trovato per ottenere proprio in una complessa base di codice C ++.
Troppe persone spesso usano solo risorse conformi alla RAII in C ++ e pensano che sia abbastanza sicuro per le eccezioni. Spesso non lo è, dal momento che una funzione può generalmente causare effetti collaterali a stati oltre quelli locali al suo ambito. In questi casi in genere è necessario iniziare a occuparsi delle protezioni dell'ambito e della sofisticata logica di rollback. Questa struttura di dati lo ha reso quindi spesso non ho bisogno di preoccuparmene poiché le funzioni non causano effetti collaterali. Stanno restituendo copie immutabili trasformate dello stato dell'applicazione invece di trasformare lo stato dell'applicazione.
Editing non distruttivo
L'editing non distruttivo è fondamentalmente operazioni di stratificazione / impilamento / connessione senza toccare i dati dell'utente originale (basta inserire i dati e i dati di output senza toccare l'input). In genere è banale implementarlo con una semplice applicazione di immagine come Photoshop e potrebbe non trarre grandi benefici da questa struttura di dati poiché molte operazioni potrebbero semplicemente voler trasformare ogni pixel dell'intera immagine.
Tuttavia, con la modifica mesh non distruttiva, ad esempio, molte operazioni spesso vogliono trasformare solo una parte della mesh. Un'operazione potrebbe voler solo spostare alcuni vertici qui. Un altro potrebbe semplicemente voler suddividere alcuni poligoni lì. Qui la struttura dei dati immutabile aiuta moltissimo a evitare la necessità di fare un'intera copia dell'intera mesh solo per restituire una nuova versione della mesh con una piccola parte di essa modificata.
Riduzione al minimo degli effetti collaterali
Con queste strutture a portata di mano, semplifica anche la scrittura di funzioni che minimizzano gli effetti collaterali senza incorrere in un'enorme penalità per le prestazioni. Mi sono ritrovato a scrivere sempre più funzioni che al giorno d'oggi restituiscono strutture di dati intere immutabili per valore senza incorrere in effetti collaterali, anche quando ciò sembra un po 'dispendioso.
Ad esempio, in genere la tentazione di trasformare un gruppo di posizioni potrebbe essere quella di accettare una matrice e un elenco di oggetti e trasformarli in modo mutevole. In questi giorni mi ritrovo a restituire un nuovo elenco di oggetti.
Quando hai più funzioni come questa nel tuo sistema che non causano effetti collaterali, rende sicuramente più facile ragionare sulla sua correttezza e testarne la correttezza.
I vantaggi delle copie economiche
Ad ogni modo, queste sono le aree in cui ho trovato il maggior uso di strutture di dati immutabili (o strutture di dati persistenti). Inizialmente ho anche avuto un po 'di zelo e ho creato un albero immutabile, una lista di collegamenti immutabili e una tabella di hash immutabile, ma nel tempo raramente ho trovato molto utile per quelli. Ho trovato principalmente il maggior uso del grosso contenitore immutabile simile a matrice nel diagramma sopra.
Ho ancora un sacco di codice che funziona con i mutabili (lo trovo una necessità pratica almeno per un codice di basso livello), ma lo stato principale dell'applicazione è una gerarchia immutabile, che passa da una scena immutabile a componenti immutabili al suo interno. Alcuni dei componenti più economici vengono ancora copiati per intero, ma quelli più costosi come mesh e immagini utilizzano la struttura immutabile per consentire quelle copie parziali a basso costo delle sole parti che devono essere trasformate.
ConcurrentModificationException
che di solito è causato dallo stesso thread che muta la raccolta nello stesso thread, nel corpo di unforeach
ciclo sulla stessa collezione.