Devo ammettere che sono uno che applica tali concetti in C ++ dal linguaggio e dalla sua natura, così come dal mio dominio e persino dal modo in cui usiamo il linguaggio. Ma date queste cose, penso che i disegni immutabili siano l'aspetto meno interessante quando si tratta di raccogliere una grande quantità di benefici associati alla programmazione funzionale, come la sicurezza dei thread, la facilità di ragionamento sul sistema, trovare più riutilizzo delle funzioni (e scoprire che possiamo combinali in qualsiasi ordine senza spiacevoli sorprese), ecc.
Prendi questo semplicistico esempio C ++ (certamente non ottimizzato per semplicità per evitare di mettermi in imbarazzo di fronte a qualsiasi esperto di elaborazione delle immagini):
// Inputs an image and outputs a new one with the specified size.
Image resized_image(const Image& src, int new_w, int new_h)
{
Image dst(new_w, new_h);
for (int y=0; y < new_h; ++y)
{
for (int x=0; x < new_w; ++x)
dst[y][x] = src.sample(x / (float)new_w, y / (float)new_h);
}
return dst;
}
Mentre l'implementazione di quella funzione muta lo stato locale (e temporaneo) sotto forma di due variabili contatore e un'immagine locale temporanea in uscita, non ha effetti collaterali esterni. Immette un'immagine e ne emette una nuova. Possiamo multithread per il contenuto dei nostri cuori. È facile ragionare, è facile testare a fondo. È eccezionalmente sicuro poiché se viene generato qualcosa, la nuova immagine viene automaticamente scartata e non dobbiamo preoccuparci di ripristinare gli effetti collaterali esterni (non ci sono immagini esterne modificate al di fuori dell'ambito della funzione, per così dire).
Vedo poco da guadagnare, e potenzialmente molto da perdere, rendendo Image
immutabile nel contesto sopra, in C ++, tranne per rendere potenzialmente la funzione sopra più ingombrante da implementare, e forse un po 'meno efficiente.
Purezza
Quindi le funzioni pure (prive di effetti collaterali esterni ) sono molto interessanti per me e sottolineo l'importanza di favorirle spesso ai membri del team anche in C ++. Ma i disegni immutabili, applicati in un contesto e una sfumatura generalmente assenti, non sono per me così interessanti dal momento che, data la natura imperativa del linguaggio, è spesso utile e pratico essere in grado di mutare alcuni oggetti temporanei locali nel processo di efficienza (entrambi per sviluppatori e hardware) implementando una funzione pura.
Copia economica di strutture pesanti
La seconda proprietà più utile che trovo è la possibilità di copiare a buon mercato le strutture di dati davvero pesanti quando il costo di farlo, come spesso verrebbe sostenuto per rendere pure le funzioni, data la loro natura di input / output, non sarebbe banale. Queste non sarebbero piccole strutture che possono stare in pila. Sarebbero strutture grandi e pesanti, come l'intera Scene
per un videogioco.
In tal caso, il sovraccarico della copia potrebbe impedire opportunità di un parallelismo efficace, poiché potrebbe essere difficile parallelizzare la fisica e renderizzarla efficacemente senza bloccarsi e colli di bottiglia se la fisica sta mutando la scena che il renderizzatore sta simultaneamente cercando di disegnare, mentre contemporaneamente ha una fisica profonda copiare l'intera scena del gioco solo per produrre un fotogramma con la fisica applicata potrebbe essere altrettanto inefficace. Tuttavia, se il sistema fisico fosse "puro", nel senso che ha semplicemente immesso una scena e ne ha emessa una nuova con la fisica applicata, e tale purezza non è venuta a scapito della copia astronomica in testa, potrebbe tranquillamente operare in parallelo con la renderer senza l'uno in attesa dell'altro.
Quindi la possibilità di copiare in modo economico i dati davvero pesanti dello stato della tua applicazione e di produrre nuove versioni modificate con costi minimi di elaborazione e utilizzo della memoria può davvero aprire nuove porte per la purezza e il parallelismo efficace, e lì trovo molte lezioni da imparare da come vengono implementate le strutture di dati persistenti. Ma qualunque cosa creiamo usando tali lezioni non deve essere del tutto persistente o offrire interfacce immutabili (potrebbe usare, ad esempio, un copy-on-write o un "costruttore / transitorio"), per ottenere questa capacità di essere sporchi a buon mercato per copiare e modificare solo sezioni della copia senza raddoppiare l'uso della memoria e l'accesso alla memoria nella nostra ricerca di parallelismo e purezza nelle nostre funzioni / sistemi / pipeline.
Immutabilità
Infine c'è l'immutabilità che ritengo il meno interessante di questi tre, ma può imporre, con un pugno di ferro, quando alcuni progetti di oggetti non sono pensati per essere usati come temporali locali per una funzione pura, e invece in un contesto più ampio, un valore tipo di "purezza a livello di oggetto", come in tutti i metodi non causa più effetti collaterali esterni (non muta più le variabili membro al di fuori dell'ambito locale immediato del metodo).
E mentre lo considero il meno interessante di questi tre in linguaggi come il C ++, può certamente semplificare i test, la sicurezza dei thread e il ragionamento di oggetti non banali. Può essere un carico lavorare con la garanzia che ad un oggetto non può essere data alcuna combinazione di stati univoca al di fuori del suo costruttore, per esempio, e che possiamo passarlo liberamente, anche per riferimento / puntatore senza appoggiarci a costanza e leggere- solo iteratori e handle e simili, pur garantendo (almeno quanto più possibile nella lingua) che i suoi contenuti originali non saranno mutati.
Ma trovo che questa sia la proprietà meno interessante perché la maggior parte degli oggetti che vedo sono utili quanto usati temporaneamente, in forma mutevole, per implementare una funzione pura (o anche un concetto più ampio, come un "sistema puro" che potrebbe essere un oggetto o una serie di funziona con il massimo effetto di inserire semplicemente qualcosa e produrre qualcosa di nuovo senza toccare nient'altro), e penso che l'immutabilità portata alle estremità in un linguaggio ampiamente imperativo sia un obiettivo piuttosto controproducente. Lo applicherei con parsimonia per le parti della base di codice in cui aiuta davvero di più.
Infine:
[...] sembrerebbe che le strutture di dati persistenti non siano di per sé sufficienti per gestire scenari in cui un thread apporta una modifica visibile ad altri thread. Per questo, sembra che dobbiamo usare dispositivi come atomi, riferimenti, memoria transazionale del software o persino classici blocchi e meccanismi di sincronizzazione.
Naturalmente se il tuo progetto richiede che le modifiche (in un senso del design di fine utente) siano visibili a più thread contemporaneamente mentre si verificano, torniamo alla sincronizzazione o almeno al tavolo da disegno per elaborare alcuni modi sofisticati per gestirlo ( Ho visto alcuni esempi molto elaborati utilizzati da esperti che si occupano di questo tipo di problemi nella programmazione funzionale).
Ma ho scoperto, una volta ottenuto quel tipo di copia e capacità di produrre versioni parzialmente modificate di strutture pesanti a buon mercato, come ad esempio con strutture di dati persistenti, si aprono spesso molte porte e opportunità che potresti non ho mai pensato prima di parallelizzare il codice che può essere eseguito in modo completamente indipendente l'uno dall'altro in una sorta di pipeline parallela I / O. Anche se alcune parti dell'algoritmo devono essere di natura seriale, potresti rinviare quell'elaborazione a un singolo thread ma scoprire che appoggiarsi a questi concetti ha aperto le porte per parallelizzare facilmente e senza preoccupazioni il 90% del lavoro pesante, ad es.