Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Immutabilità

Nella programmazione funzionale, per gestire al meglio i dati ed evitare effetti collaterali, si ricorre all'immutabilità: ecco come fare su Javascript.
Nella programmazione funzionale, per gestire al meglio i dati ed evitare effetti collaterali, si ricorre all'immutabilità: ecco come fare su Javascript.
Link copiato negli appunti

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.


Ti consigliamo anche