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

Gestire gli effetti collaterali

Le tecniche per evitare effetti collaterali di difficile gestione quando si usa la programmazione funzionale in Javascript, definendo quindi funzioni pure.
Le tecniche per evitare effetti collaterali di difficile gestione quando si usa la programmazione funzionale in Javascript, definendo quindi funzioni pure.
Link copiato negli appunti

Nel descrivere le funzioni pure, abbiamo detto che esse non devono avere effetti collaterali. Ma come possiamo scrivere funzioni in modo da evitarli? Innanzitutto bisogna determinare cosa si intende per effetto collaterale. In estrema sintesi possiamo definirlo come una interazione con il mondo esterno diversa dal ricevimento dei valori di input e dalla restituzione del risultato.

Come evitare gli effetti collaterali

Tra i più comuni esempi di effetti collaterali che rendono una funzione non pura segnaliamo:

  • le chiamate AJAX;
  • le interazioni con i dispositivi, come ad esempio tastiere, mouse, console, ecc.;
  • l'accesso a dati esterni, come ad esempio cookie, DOM, localStorage, ecc.;
  • l'interazione con il sistema, ad esempio la richiesta della data e ora corrente o la generazione di un numero casuale tramite Math.random().

Facciamo notare che anche la chiamata ad una funzione non pura rende la funzione corrente non pura, dal momento che in questo caso la predicibilità del risultato viene inevitabilmente compromessa.

Inoltre, il vincolo di non interagire con l'ambiente esterno deve essere rispettato non solo evitando di accedere a variabili esterne allo scope della funzione, ma anche evitando di modificare i parametri stessi della funzione. Consideriamo il seguente esempio:

function addItem(list, item) {
   list.push(item);
   return list;
}

Supponiamo di avere un array di interi e di voler aggiungere un nuovo intero sfruttando questa funzione:

var listaInteri = [1, 2, 3];
console.log(addItem(listaInteri, 4));
//risultato: [1, 2, 3, 4]

Il risultato dell'esecuzione di addItem() aggiungerà il numero 4 all'array listaInteri, come ci aspettavamo. Possiamo affermare che questa funzione sia pura?

Proviamo ad eseguire nuovamente la chiamata con gli stessi parametri:

console.log(addItem(listaInteri, 4));
//risultato: [1, 2, 3, 4, 4]

Il risultato ottenuto non è identico al precedente, quindi non possiamo affermare che addItem() sia una funzione pura. Ma da cosa dipende questa difformità nel risultato restituito? Il problema dipende proprio dal fatto che nel corpo della funzione viene generato un effetto collaterale modificando il parametro list. Dal momento che l'array viene passato per riferimento, questa modifica si riflette all'esterno della funzione. Quindi il risultato della funzione sarà non solo la l'array restituito, ma anche la modifica dell'array ricevuto in input.

Come risulta evidente da questo esempio, un effetto collaterale di questo tipo può essere fonte di malfunzionamenti subdoli, talvolta difficili da diagnosticare.

Trasformare funzioni non-pure in funzioni pure

Possiamo trasformare una funzione non pura in una funzione pura utilizzando diverse tecniche.

Ad esempio, il caso precedente può essere risolto lavorando su una copia dell'array ricevuto in input, come mostrato di seguito:

function addItem(list, item) {
   var myList = list.slice(0);
   myList.push(item);
   return myList;
}

Come possiamo vedere, abbiamo creato una copia di list fruttando il metodo slice() ed abbiamo lavorato sulla copia, mantenendo intatto il parametro di input. Questo evita l'effetto collaterale che abbiamo evidenziato prima, ma la soluzione non è definitiva. Infatti slice() può essere utile per array contenenti valori primitivi, come numeri, stringhe e booleani. Ma il problema dell'effetto collaterale si ripresenta per array di oggetti, dal momento che la copia degli elementi dell'array viene fatta per riferimento. Una possibile soluzione potrebbe essere quella di serializzare e deserializzare l'array o di utilizzare funzioni di deep copying, ma l'argomento esula dallo scopo di questo articolo. La cosa rilevante è che bisogna assicurarsi di non modificare i parametri di input e quindi eventualmente di lavorare su copie dei parametri.

Un altro approccio per evitare effetti collaterali consiste nell'iniettare le eventuali dipendenze esterne in maniera esplicita. Per chiarire, riprendiamo l'esempio della funzione add():

var value = 0;
function add(n) {
   value = value + n;
   return value;
}
add(1); //result: 1
add(1); //result: 2

Come abbiamo visto prima, il fatto che all'interno della funzione si vada a modificare una variabile definita esternamente alla funzione la rende non pura. Possiamo trasformare questa funzione in una funzione pura cambiando leggermente la semantica, come mostrato di seguito:

var value = 0;
function add(currentValue, n) {
   currentValue = currentValue + n;
   return currentValue;
}
value = add(value, 1); //result: 1
value = add(value, 1); //result: 2

Abbiamo aggiunto il parametro value e dato un nuovo significato alla funzione: invece di aggiungere il valore n alla variabile implicita value, alla nuova funzione viene richiesto di aggiungere esplicitamente al valore di value il valore di n e restituire il nuovo valore. Questa nuova funzione non interagirà più con la variabile esterna, ma fornirà soltanto il risultato. Sarà compito di chi chiama add() di aggiornare la variabile globale. Anche se il risultato finale è identico a quello precedente, abbiamo ottenuto il vantaggio di rendere la funzione add() pura, e quindi deterministica e testabile.

Effetti collaterali locali

Abbiamo detto che una funzione pura non deve avere effetti collaterali. Consideriamo la seguente funzione:

function sum(listOfIntegers) {
   let result = 0;
   for (let i of listOfIntegers) {
      result = result + i;
   }
   return result;
}

Essa restituisce la somma degli interi contenuti in un array. Il calcolo si basa su un ciclo ed una variabile che fa da accumulatore delle somme parziali. Ad ogni iterazione del ciclo viene modificata la variabile locale result, ed in effetti questo è un effetto collaterale delle istruzioni del ciclo. In altre parole, se considerassimo il ciclo for come una funzione, essa non sarebbe pura. Tuttavia, l'effetto esterno della funzione sum() è quello di una funzione pura: a parità di input otterremo sempre lo stesso output. L'effetto collaterale locale non influenza l'ambiente esterno, per cui possiamo accettare situazioni di questo tipo, in quanto viene mantenuta la trasparenza referenziale.

La necessità di funzioni non-pure

Parlando della distinzione tra funzioni pure e non pure, abbiamo messo in risalto il fatto che le funzioni pure presentano una serie di vantaggi, come la testabilità, la semplicità di refactoring o l'applicazione di tecniche di ottimizzazione come la memoization. Abbiamo anche visto come in alcuni casi è possibile trasformare una funzione non pura in una funzione pura. Questi concetti sembrerebbero trasmettere l'idea che la modifica di un ambiente esterno tramite funzioni non pure sia sempre da evitare. Naturalmente questa idea è estrema. Nella pratica non avremo mai applicazioni che non necessitano di fare riferimento ad un ambiente esterno: basti pensare alla semplice gestione dell'input e dell'output.

L'idea della programmazione funzionale non è quella di eliminare l'esistenza delle funzioni non pure. L'idea è invece di scrivere quanto più possibile funzioni pure e deterministiche e ricorrere al minimo indispensabile, ma necessario, per generare effetti collaterali ed interagire con il mondo esterno.


Ti consigliamo anche