- Learn
- Guida Functional Javascript
- Immutabilità
Immutabilità
Uno dei principi cardine della programmazione funzionale è l’assenza di effetti collaterali nell’esecuzione del codice. Abbiamo visto nelle lezioni precedenti quali sono i vantaggi e come possiamo limitare la loro presenza. Purtroppo il funzionamento stesso di alcune semplici istruzioni del linguaggio (ma non solo di JavaScript) ci mettono in situazioni in cui non possiamo fare a meno di generare effetti collaterali.
Prendiamo ad esempio l’assegnamento. Per tipi di dato primitivi, come numeri, stringhe, booleani, ecc., l’assegnamento di un valore ad una variabile è immune da potenziali effetti collaterali. Consideriamo il seguente esempio:
var x = 32;
var y = x;
x = 12;
console.log(x); //12
console.log(y); //32
Il fatto di aver assegnato alla variabile y il valore della variabile x non vincola le due variabili. Infatti, la modifica del valore di x non ha influenza sul valore di y, che rimane quello iniziale di 32.
Proviamo a fare un’operazione analoga per tipi di dato complessi, come ad esempio un oggetto:
var mario = {nome: "Mario", cognome: "Rossi"};
var gigi = mario;
gigi.nome = "Gigi";
console.log(mario); //{nome: "Gigi", cognome: "Rossi"}
console.log(gigi); //{nome: "Gigi", cognome: "Rossi"}
In questo caso una modifica ad una proprietà dell’oggetto gigi ha ripercussioni anche sull’oggetto mario. Com’è noto, il motivo risiede nel fatto che per tipi di dato complessi come array e oggetti, l’assegnamento ad una variabile consiste in realtà nell’assegnamento del puntatore della struttura dati. Quello che accade è che più variabili puntano alla stessa struttura dati e che modifiche apportate ad un suo elemento siano volontariamente o involontariamente condivise con altre parti di un’applicazione. In altre parole, il semplice assegnamento di tipi di dati complessi è predisposto alla generazione di effetti collaterali e quindi a potenziali bug di difficile identificazione.
Per contrastare la generazione di effetti collaterali nell’assegnamento di variabili, il modello funzionale propone il concetto di immutabilità, cioè il fatto che un valore non può essere mutato, modificato. In questo contesto le variabili rappresentano un singolo valore non modificabile: se qualcosa è stato dichiarato che debba avere un determinato valore, il resto del codice deve avere la garanzia che continuerà a mantenere quel valore. Il principio è che se due elementi rappresentano valori diversi, allora devono essere rappresentati da variabili diverse.
Uso di costanti
Un primo approccio nel tentare di far rispettare questo principio in JavaScript consiste proprio nel limitare l’uso di variabili e nell’usare al loro posto le costanti. Consideriamo il seguente esempio:
const x = 32;
const y = x;
x = 12;
L’esecuzione dell’ultimo assegnamento genererà un’eccezione, dal momento che si sta tentando di assegnare un nuovo valore ad una costante. In questo modo possiamo decidere consciamente se x deve poter variare o si tratta di un assegnamento accidentale, e quindi causa di potenziali bug.
Prendere l’abitudine di usare costanti per dichiarare valori può aiutarci quindi nel prevenire mutazioni indesiderate. Se nell’esempio precedente volevamo effettivamente assegnare un nuovo valore a x, cambieremo il codice come mostrato di seguito, ma solo ciò fosse veramente necessario:
let x = 32;
const y = x;
x = 12;
L’utilizzo delle costanti non è però sufficiente, soprattutto quando abbiamo a che fare con strutture complesse. Consideriamo ad esempio il seguente codice:
const mario = {nome: "Mario", cognome: "Rossi"};
const gigi = mario;
gigi.nome = "Gigi";
L’ultimo assegnamento non genererà alcuna eccezione e ci ritroveremo la proprietà nome dell’oggetto mario con un nuovo valore nonostante avessimo dichiarato mario e gigi come costanti. Il fatto è che l’aver dichiarato un oggetto come costante ci garantisce che il puntatore alla struttura dati che rappresenta l’oggetto non può cambiare, ma non da alcuna garanzia sulle modifiche alle sue proprietà ed alla sua struttura. Quindi in questo caso il ricorso a costanti è una misura troppo debole per garantire l’immutabilità dei valori.
Freezing degli oggetti
Possiamo ottenere un certo grado di immutabilità per gli oggetti ricorrendo
al metodo Object.freeze()
. Il seguente è un esempio d’uso di questo metodo:
const mario = {nome: "Mario", cognome: "Rossi"};
Object.freeze(mario);
mario.nome = "Gigi";
L’ultimo assegnamento non genererà un’eccezione, ma non avrà alcun effetto
sulla proprietà nome dell’oggetto mario. In altre parole
l’oggetto mario è immutabile, quindi sembrerebbe la soluzione
ideale per evitare effetti collaterali. Purtroppo il metodo Object.freeze()
blocca le modifiche soltanto per le proprietà
dirette dell’oggetto. Consideriamo infatti il seguente esempio:
const mario = {
nome: "Mario",
cognome: "Rossi",
indirizzo: {
via: "Via Garibaldi",
numero: "11",
citta: "Roma"
}
};
Object.freeze(mario);
mario.indirizzo.numero = "42";
Se ispezioniamo l’oggetto mario dopo l’esecuzione di questo codice,
noteremo con sorpresa che il numero dell’indirizzo è cambiato. Infatti, il
metodo Object.freeze()
blocca soltanto le mutazioni al primo
livello delle proprietà di un oggetto, non garantendo quindi una
immutabilità completa.
Potremmo implementare una versione di Object.freeze()
che rende
immutabili anche le proprietà di tipo oggetto procedendo ricorsivamente,
come mostrato dal seguente codice:
function deepFreeze(object) {
var propNames = Object.getOwnPropertyNames(object);
for (let name of propNames) {
let value = object[name];
object[name] = value && typeof value === "object" ? deepFreeze(value) : value;
}
return Object.freeze(object);
}
Tuttavia la funzione deepFreeze()
corre il rischio di entrare in
un loop infinito se la struttura dell’oggetto ha qualche riferimento
circolare.
In conclusione, JavaScript non supporta nativamente l’immutabilità. Possiamo ottenere un certo grado di immutabilità sfruttando alcuni semplici accorgimenti. Se vogliamo andare oltre, possiamo affidarci a librerie specifiche come ad esempio Immutable.js e mori.
Copie al posto di mutazioni
Naturalmente l’immutabilità dei dati appare come contro-intuitiva nella programmazione a cui siamo abituiti, dove inevitabilmente abbiamo bisogno di apportare modifiche. Se dobbiamo preferire l’immutabilità dei dati al posto delle loro mutazioni, come possiamo elaborarli nelle nostre applicazioni?
Il principio di fondo è che nel paradigma funzionale un valore rappresenta lo stato in un particolare istante ed una variabile non è altro che un alias di quel valore. Nella programmazione imperativa, invece, una variabile rappresenta un contenitore per lo stato il cui valore cambia nel tempo.
Pertanto l’alternativa alla mutazione dei dati e la creazione di nuovi dati al posto della modifica di quelli esistenti, la generazione di un nuovo stato invece che la modifica dello stato corrente.
Il ragionamento è analogo a quello che avevamo fatto per le funzioni pure: l’applicazione di una funzione pura non deve modificare i dati di input. Quindi se abbiamo una funzione che prende in input un oggetto per farci delle elaborazioni, questa funzione deve restituire un nuovo oggetto, come nel seguente esempio:
function incrementaEta(persona, numeroAnni) {
return Object.assign({}, persona, {
eta: persona.eta + numeroAnni
});
}
La funzione incrementaEta()
dell’esempio prende in input un
oggetto persona ed un numero che rappresenta il numero di anni di
cui incrementare l’età della persona. Come possiamo vedere, la funzione non
lavora direttamente sull’oggetto persona passato come primo
parametro. Viene infatti effettuata una copia tramite Object.assign()
sostituendo la proprietà eta con il nuovo
valore. Il risultato finale è un nuovo oggetto persona, distinto
dall’oggetto passato come parametro che infatti rimane immutato.
Oltre che sugli oggetti, possiamo applicare un metodo analogo anche sugli
array sfruttando lo spread operator (...
). Consideriamo il
seguente codice:
function addItem(array, item) {
return [...array, item];
}
Quello che ci aspettiamo è che la funzione addItem()
restituisca
un array composto dal contenuto dell’array passato come primo parametro
seguito dall’elemento passato come secondo parametro. Come per la funzione incrementaEta()
, l’array originale non viene modificato. In questo
modo abbiamo ottenuto una versione pura del metodo push()
.
Occorre segnalare che sia Object.assign()
che lo spread operator non effettuano copie degli oggetti annidati o
degli oggetti contenuti negli array. Quindi sarebbe opportuno conoscere la
struttura degli oggetti da clonare per ottenere una immutabilità effettiva.
Se vuoi aggiornamenti su JavaScript inserisci la tua email nel box qui sotto:
Compilando il presente form acconsento a ricevere le informazioni relative ai servizi di cui alla presente pagina ai sensi dell'informativa sulla privacy.
La tua iscrizione è andata a buon fine. Se vuoi ricevere informazioni personalizzate compila anche i seguenti campi opzionali:
Compilando il presente form acconsento a ricevere le informazioni relative ai servizi di cui alla presente pagina ai sensi dell'informativa sulla privacy.
I Video di HTML.it
Come creare animazioni in HTML5 e fogli di stile CSS
In questo video è mostrato come modificare un oggetto in HTML5 eseguendo un’animazione dopo un evento definito. Nel tutorial, vedremo […]