Solo i tipi generalmente immutabili creati in linguaggi che non ruotano attorno all'immutabilità tenderanno a costare più tempo per lo sviluppo degli sviluppatori e potrebbero essere potenzialmente utilizzati se richiedono un tipo di oggetto "costruttore" per esprimere le modifiche desiderate (ciò non significa che il il lavoro sarà maggiore, ma in questi casi c'è un costo iniziale. Inoltre, indipendentemente dal fatto che la lingua renda davvero semplice la creazione di tipi immutabili o meno, tenderà a richiedere sempre un po 'di elaborazione e sovraccarico di memoria per tipi di dati non banali.
Rendere le funzioni prive di effetti collaterali
Se stai lavorando in lingue che non ruotano attorno all'immutabilità, penso che l'approccio pragmatico non sia quello di cercare di rendere immutabile ogni singolo tipo di dati. Una mentalità potenzialmente molto più produttiva che offre molti degli stessi vantaggi è quella di concentrarsi sulla massimizzazione del numero di funzioni nel sistema che causano zero effetti collaterali .
Ad esempio, se hai una funzione che provoca un effetto collaterale come questo:
// Make 'x' the absolute value of itself.
void make_abs(int& x);
Quindi non abbiamo bisogno di un tipo di dati integer immutabile che proibisca agli operatori come l'assegnazione post-inizializzazione di evitare che la funzione eviti effetti collaterali. Possiamo semplicemente farlo:
// Returns the absolute value of 'x'.
int abs(int x);
Ora la funzione non interferisce con x
nulla al di fuori del suo ambito di applicazione, e in questo banale caso potremmo anche aver rasato alcuni cicli evitando qualsiasi sovraccarico associato all'indirizzamento / aliasing. Almeno la seconda versione non dovrebbe essere più computazionalmente costosa della prima.
Cose che sono costose da copiare per intero
Naturalmente la maggior parte dei casi non è così banale se vogliamo evitare che una funzione causi effetti collaterali. Un caso d'uso complesso e complesso potrebbe essere più simile a questo:
// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);
A quel punto la mesh potrebbe richiedere circa duecento megabyte di memoria con oltre centomila poligoni, ancora più vertici e spigoli, mappe di texture multiple, target di morph, ecc. Sarebbe davvero costoso copiare l'intera mesh solo per fare questo transform
senza effetti collaterali, in questo modo:
// Returns a new version of the mesh whose vertices been
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);
Ed è in questi casi in cui la copia di qualcosa nella sua interezza sarebbe normalmente un sovraccarico epico in cui ho trovato utile trasformarsi Mesh
in una struttura di dati persistente e un tipo immutabile con il "builder" analogico per creare versioni modificate di esso in modo che può semplicemente copiare parti di conteggio e riferimento non contigue. È tutto incentrato sulla capacità di scrivere funzioni mesh che sono prive di effetti collaterali.
Strutture di dati persistenti
E in questi casi in cui copiare tutto è così incredibilmente costoso, ho trovato lo sforzo di progettare un modello immutabile Mesh
per pagare davvero, anche se aveva un costo leggermente elevato in anticipo, perché non semplificava solo la sicurezza del thread. Ha inoltre semplificato la modifica non distruttiva (che consente all'utente di sovrapporre le operazioni di mesh senza modificare la sua copia originale), di annullare i sistemi (ora il sistema di annullamento può semplicemente archiviare una copia immutabile della mesh prima delle modifiche apportate da un'operazione senza far saltare la memoria uso) e sicurezza delle eccezioni (ora se si verifica un'eccezione nella funzione sopra, la funzione non deve tornare indietro e annullare tutti i suoi effetti collaterali poiché non ha causato alcun inizio).
Posso affermare con certezza in questi casi che il tempo necessario per rendere immutabili queste strutture di dati pesanti ha permesso di risparmiare più tempo di quanto non sia costato, poiché ho confrontato i costi di manutenzione di questi nuovi progetti con quelli precedenti che ruotavano attorno alla mutabilità e alle funzioni che causavano effetti collaterali, e i precedenti progetti mutabili costano molto più tempo ed erano molto più inclini all'errore umano, specialmente in aree che sono davvero allettanti per gli sviluppatori da trascurare durante il periodo di crisi, come la sicurezza delle eccezioni.
Quindi penso che in questi casi i tipi di dati immutabili paghino davvero, ma non tutto deve essere reso immutabile per rendere la maggior parte delle funzioni del sistema prive di effetti collaterali. Molte cose sono abbastanza economiche da copiarle per intero. Inoltre, molte applicazioni del mondo reale dovranno causare alcuni effetti collaterali qua e là (almeno come il salvataggio di un file), ma in genere ci sono molte più funzioni che potrebbero essere prive di effetti collaterali.
Il punto di avere alcuni tipi di dati immutabili per me è assicurarmi di poter scrivere il numero massimo di funzioni per essere liberi da effetti collaterali senza incorrere in spese generali epiche sotto forma di copia profonda di enormi strutture di dati a sinistra ea destra per intero quando solo piccole porzioni di loro devono essere modificati. Avere strutture di dati persistenti in questi casi finisce per diventare un dettaglio di ottimizzazione per permetterci di scrivere le nostre funzioni in modo da evitare effetti collaterali senza pagare un costo epico per farlo.
Immutabile sovraccarico
Ora concettualmente le versioni mutabili avranno sempre un vantaggio in termini di efficienza. C'è sempre quel sovraccarico computazionale associato a strutture di dati immutabili. Ma l'ho trovato uno scambio degno nei casi che ho descritto sopra, e puoi concentrarti sul rendere il sovraccarico di natura sufficientemente minimale. Preferisco quel tipo di approccio in cui la correttezza diventa facile e l'ottimizzazione diventa più difficile piuttosto che l'ottimizzazione diventa più facile, ma la correttezza diventa più difficile. Non è altrettanto demoralizzante avere un codice che funzioni perfettamente correttamente e necessiti di qualche messa a punto in più sul codice che non funziona correttamente in primo luogo, non importa quanto velocemente raggiunga i suoi risultati errati.