Programmazione Funzionale in JavaScript: Composizione

P

cos’è La programmazione funzionale?

JavaScript, oltre a permettere la manipolazione del DOM, è un linguaggio funzionale con caratteristiche avanzate per lo sviluppo di applicazioni che necessitano di manipolare e organizzare grandi quantità di informazioni. I grandi progetti devono essere ordinati e scalabili. Senza queste qualità un progetto non sarà in grado di evolvere né di adattarsi alle nuove richieste degli utenti, i quali hanno aspettative sempre più alte sulla velocità e la stabilità dei siti che visitano.

“La programmazione funzionale non è altro che programmare con funzioni”

Uno dei principi più importanti della programmazione funzionale è la “purezza” con cui devono essere ideate le funzioni. Una funzione nella quale il valore ritornato dipende esclusivamente dagli argomenti, e che non genera nessun “side effect“, si può definire come una funzione pura. Si parla invece di funzione impura quando una funzione non calcola il valore che restituirà, bensì realizza un’altra operazione.

I “side effect” delle funzioni avvengono quando si modificano i dati (per esempio sottratti da un database) del programma, e quando si cambiano i valori di alcune variabili presenti al fuori dell’ambito della funzione. Potrebbe accedere quindi di modificare un valore da cui potrebbero dipendere altre parti del nostro programma.

Un esempio di funzione impura:

var num = 10;

let sommaImpura = (a) => {
  num += a;
  return num;
}
console.log(sommaImpura(5), num);

// sommaImpura(5) -> 15
// num -> 15

La funzione sommaImpura sta modificando un valore globale che, come già detto in precedenza, può causare dei problemi in altri parti del programma che dipendono dal valore della variabile num. Un altro modo con cui è possibile scrivere la funzione sommaImpura è il seguente:

var num = 10;

let sommaImpura = (a) => {
  return num + a;
}
console.log(sommaImpura(8), num);

// sommaImpura(8) -> 18
// num -> 10

Sebbene abbiamo risolto il problema della modifica sulla variabile num, questa funzione è ancora impura perché dipende da un valore globale e non esclusivamente dagli argomenti. Una funzione pura deve ritornare lo stesso valore per ogni invocazione che utilizza  il medesimo argomento. Un altro esempio di funzione impura è il seguente:

var num = 10;

let sommaImpura = (a) => {
  return num + a;
}
console.log(sommaImpura(5)) // 15
num = 20;

console.log(sommaImpura(5)) // 25

La variabile num è stata cambiata da un’altra parte del programma e per questo motivo anche se l’argomento dato alla funzione è lo stesso, il valore ritornato è diverso. Quindi, come possiamo realmente trasformare questa funzione impura in una funzione pura? Una possibile soluzione è la seguente:

var num = 10;
let sommaPura = (a, b) => {
  return a + b;
}

sommaPura(num, 5); // 15

num = 20;

sommaPura(num, 5); // 25

Questa funzione è definibile come pura perché dipende soltanto dai suoi argomenti, quindi se è invocata più volte utilizzando i medesimi argomenti ritornerà sempre lo stesso valore. Questo è un esempio della funzione somma(a,b) => {return a+b}che conosciamo tutti. Sapevate che era 100% funzionale? Come si può vedere usiamo la programmazione funzionale senza rendercene nemmeno conto.

Programmazione funzionale in JavaScript

Esistono 3 metodi principali per la programmazione funzionale in JavaScript: filter, map, reduce. Tutti questi sono metodi applicabili sugli array. Facendo uso di questi tre metodi potremmo estrarre le informazioni da un array e dagli elementi che contiene per creare nuovi array senza modificare l’array originale (l’informazione non deve essere modificata).

utilizzO DEI metodi funzionali

Immaginiamo di aver estratto queste informazioni da un database:

const comuni = [
  {
    nome: 'Treviso',
    regione: 'Veneto',
    abitanti: 84200
  },
  {
    nome: 'Siena',
    regione: 'Toscana',
    abitanti: 53693
  },
  {
    nome: 'Pescara',
    regione: 'Abruzzo',
    abitanti: 120286
  },
  {
    nome: 'Catania',
    regione: 'Sicilia',
    abitanti: 236231
  },
  {
    nome: 'Torino',
    regione: 'Piemonte',
    abitanti: 885651
  }
]

Ad esempio potrebbero servici solo i comuni con una popolazione superiore a 100.000 abitanti. Grazie al metodo filter possiamo creare un nuovo array con questo requisito senza modificare l’array originale:

const ottenereComuniPopolati = (arr) => arr.filter(( {abitanti} ) => abitanti > 100000); 
// {abitanti} è destrutturazione, sarebbe la stessa cosa di obj.abitanti
var comuniPopolati = ottenereComuniPopolati(comuni);

/* 
Risultato di comuniPopolati:
[
  {
    "nome": "Pescara",
    "regione": "Abruzzo",
    "abitanti": 120286
  },
  {
    "nome": "Catania",
    "regione": "Sicilia",
    "abitanti": 236231
  },
  {
    "nome": "Torino",
    "regione": "Piemonte",
    "abitanti": 885651
  }
]


docs destrutturazione: 
https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment 

*/

Filter ha preso soltanto gli oggetti con un numero di abitanti superiore a 100000 e ha creato un nuovo array pronto per l’uso. Dato che l’array originale non è stato modificato questo metodo non ha cambiato nessun valore, quindi non ha generato nessun side effect.

Adesso sappiamo che comuniPopolati possiede i comuni più popolati del nostro array originale (comuni). E se volessimo soltanto estrarre la regione di questi comuni e nient’altro? Nel precedente esempio filter ritorna tutto il contenuto dell’oggetto (nome, regione e abitanti) quando determina che gli abitanti sono superiori a 100000. Il metodo map, invece, non valuta nessuna condizione ma crea un nuovo array mappando tutti gli elementi e ritornando solo le caratteristiche specificate da noi.

Con l’utilizzo del metodo map risolveremmo il nostro problema in questo modo:

const ottenereTutteLeRegioni = (arr) => arr.map(( {regione} ) => regione);
let leRegioniPopolati = ottenereTutteLeRegioni(comuniPopolati);

/* Risultato di leRegioniPopolati:
[
  "Abruzzo",
  "Sicilia",
  "Piemonte"
]
*/

// Anche potremmo sottrarli come oggetti dentro un array:

const ottenereTutteLeRegioniOBJ = (arr) => arr.map(( {regione} ) => ( {regione} ));
let leRegioniPopolatiOBJ = ottenereTutteLeRegioniOBJ(comuniPopolati);

/* Risultato di leRegioniPopolatiOBJ:

[
  {
    "regione": "Abruzzo"
  },
  {
    "regione": "Sicilia"
  },
  {
    "regione": "Piemonte"
  }
]

*/

Possiamo anche ridurre/trasformare un array in un qualsiasi valore con l’utilizzo del metodo reduce, il quale realizza una iterazione su ogni elemento contenuto in un array e ritorna un valore che verrà usato nella prossima iterazione.

Utilizzando il nostro array comuni:

const comuni = [
  {
    nome: 'Treviso',
    regione: 'Veneto',
    abitanti: 84200
  },
  {
    nome: 'Siena',
    regione: 'Toscana',
    abitanti: 53693
  },
  {
    nome: 'Pescara',
    regione: 'Abruzzo',
    abitanti: 120286
  },
  {
    nome: 'Catania',
    regione: 'Sicilia',
    abitanti: 236231
  },
  {
    nome: 'Torino',
    regione: 'Piemonte',
    abitanti: 885651
  }
]

Sommeremo tutti i nostri abitanti al fine di conoscere il totale della popolazione di tutti i comuni dentro il nostro array:

const ottenereGliAbitanti = (arr) => arr.reduce((arg, {abitanti}) => arg + abitanti, 0);

var popolazioneTotale = ottenereGliAbitanti(comuni); // 1380061

Il metodo metodo reduce accetta due argomenti:

  • il primo è una funzione (callback) che verrà invocata su ogni elemento dell’array,
  • il secondo parametro è il valore iniziale dell’iterazione sull’array.

La callback, parametro di reduce, ha, a sua volta, alcuni parametri:

  • arg, che rappresenta il valore di accumulo (alla prima iterazione è uguale al valore di inizializzazione di reduce),
  • il valore corrente dell’iterazione sull’array (tramite sintassi { abitanti } stiamo prelevando la proprietà “abitanti” dell’oggetto)

In questo modo reduce ritorna arg + abitanti:

  1. Prima iterazione: arg sarà 0 e abitanti sarà il valore della proprietà abitanti nel oggetto attuale. Il valore di abitanti sarà 84200 perché è la prima iterazione (la stessa cosa di comuni[0].abitanti). Ritorniamo arg che sarà: 0 + 84200
  2. Seconda iterazione: arg sarà 84200 e abitanti sarà 53693 (comuni[1].abitanti). Ritorniamo arg: 84200 + 53693
  3. Terza iterazione: arg = 137893; abitanti = 120286 (comuni[2].abitanti). Ritorniamo arg: 137893 + 120286
  4. Quarta iterazione: arg = 258179; abitanti = 236231 (comuni[3].abitanti). Ritorniamo arg: 258179 + 236231
  5. Quinta iterazione: arg = 494410; abitanti = 885651 (comuni[4].abitanti). Ritorniamo arg: 494410 + 885651
  6. Fine, non ci sono più oggetti da iterare. Risultato: 1380061

Il secondo argomento di reduce(funzione, secondoArgomento), può essere qualsiasi cosa: oggetti, strings, arrays, numeri, funzioni oppure niente… Se vuoi approfondire su questa funzione ti consiglio di leggere la documentazione ufficiale.

 

Composizione in JavaScript.

La composizione FUNZIONALE CONSISTE NEL combinare VARIE funzioni semplici per crearne una più complessa

Wikipedia

Una funzione che permette combinare altre funzioni è strutturata in questa maniera:

const composizione = (g, f) => (arg) => f(g(arg))

Innanzitutto, composizione è una funzione che ha bisogno di due funzioni come argomenti; dopo avere specificato queste due funzioni, composizione ritorna un’altra funzione che aspetta un argomento. Nel momento in cui riceve questo argomento, la seconda funzione ritorna il valore dell’esecuzione di f(g(arg)). Tutto questo si può riassumere nel seguente modo: g(arg) processa l’argomento e ritorna un valore che verrà poi processato dalla nostra seconda funzione f.

Facciamo un esempio pratico:

Faremo uso del nostro metodo ottenereComuniPopolati il quale crea un nuovo array con i comuni che hanno una popolazione superiore a 100000. Inoltre useremo il nostro metodo ottenereGliAbitanti che somma la proprietà abitanti di ogni oggetto dentro un array.

Possiamo ottenere il totale della popolazione dei comuni più popolati facendo uso della composizione:

let popolazioneDiComuniMoltoPopolati = composizione(ottenereComuniPopolati, ottenereGliAbitanti);

Utilizziamo la composizione in questo modo:

let popolazioneDiComuniMoltoPopolati = composizione(ottenereComuniPopolati, ottenereGliAbitanti);

// Possiamo adesso chiamare popolazioneDiComuniMoltoPopolati e passare il nostro array comuni. 
popolazioneDiComuniMoltoPopolati(comuni) // 1242168

Rappresentazione del funzionamento del codice riportato sopra:

let popolazioneDiComuniMoltoPopolati = composizione(ottenereComuniPopolati, ottenereGliAbitanti);

/* rappresentazione della funzione ritornata da composizione: */
popolazioneDiComuniMoltoPopolati = (arg) => ottenereGliAbitanti(ottenereComuniPopolati(arg));
/*
popolazioneDiComuniMoltoPopolati è diventata una funzione che aspetta un argomento. Questo  argomento sarà il nostro array comuni da dove verranno ottenuti i comuni più popolati grazie alla funzione "ottenereComuniPopolati". L'array ritornato da "ottenereComuniPopolati" sarà essatamente questo:
[
  {
    "nome": "Pescara",
    "regione": "Abruzzo",
    "abitanti": 120286
  },
  {
    "nome": "Catania",
    "regione": "Sicilia",
    "abitanti": 236231
  },
  {
    "nome": "Torino",
    "regione": "Piemonte",
    "abitanti": 885651
  }
]

   dopo, questo array sarà processato da "ottenereGliAbitanti" che ritornerà il totale della popolazione attraverso la somma degli abitanti, risultato: 1242168

*/

popolazioneDiComuniMoltoPopolati(comuni) // 1242168

Composizione avanzata.

La composizione non è limitata alla sola possibilità  di “raggruppare” due funzioni dentro un’altra. Possono essere raggruppate più funzioni, ma per fare questo dobbiamo fare dei piccoli cambiamenti alla nostra funzione composizione:

const _composizione = (...fns) => (arg) => fns.reduce((refer, fn) => fn(refer), arg)

Ora, _composizione permette l’uso di più funzioni da specificare come primo argomento, e la seconda funzione segue aspettando un argomento. C’è una piccola modifica rispetto all’esempio precedente: l’uso di reduce al posto del raggruppamento di funzioni (il funzionamento di reduce in questa nuova _composizione verrà spiegato più avanti). Adesso però andiamo ad utilizzare la nostra funzione:

let popolazioneDiComuniMoltoPopolati = _composizione(ottenereComuniPopolati, ottenereGliAbitanti)

popolazioneDiComuniMoltoPopolati(comuni) // 1242168

 

COMPOSIZIONI E reduce

const _composizione = (...fns) => (arg) => fns.reduce((refer, fn) => fn(refer), arg);


// spiegazione di:
fns.reduce((refer, fn) => fn(refer), arg);
  • fns è un array con tutte le funzioni che abbiamo passato alla funzione _composizione
  • arg è l’argomento che viene stabilito dalla funzione ritornata da _composizione quando abbiamo già passato tutte le nostre funzioni
  • refer è un riferimento all’argomento (arg)

Utilizzando questa istruzione:

popolazioneDiComuniMoltoPopolati(comuni)

vengono eseguite le seguenti istruzioni:

fns.reduce((referenzaComuni, primaFunzioneNelNostroArrayFNS) => primaFunzioneNelNostroArrayFNS(referenzaComuni), comuni)

Quindi la prima funzione nel nostro array fns è ottenereComuniPopolati perché è il primo argomento utilizzato:

let popolazioneDiComuniMoltoPopolati = _composizione(ottenereComuniPopolati, ottenereGliAbitanti)
popolazioneDiComuniMoltoPopolati(comuni)

La prima iterazione di reduce somiglia a:

fns.reduce((referComuni, ottenereComuniPopolati) => ottenereComuniPopolati(referComuni), comuni)

Il risultato ritornato da questa prima iterazione:

[
  {
    "nome": "Pescara",
    "regione": "Abruzzo",
    "abitanti": 120286
  },
  {
    "nome": "Catania",
    "regione": "Sicilia",
    "abitanti": 236231
  },
  {
    "nome": "Torino",
    "regione": "Piemonte",
    "abitanti": 885651
  }
]

Nella seconda iterazione, utilizzando il risultato della prima iterazione, viene eseguito:

fns.reduce((referComuni, ottenereGliAbitanti) => ottenereGliAbitanti(referComuni), comuni)

ottenereGliAbitanti ritorna la somma di tutti gli abitanti e reduce termina perché non ci sono altre funzioni dentro l’array.

 

CONCLUSIONE

La programmazione funzionale è più intuitiva e offre modi sempre più semplici per la soluzione di qualsiasi problema. Inoltre i programmi tendono ad essere più compatti e pertanto più facili da capire. Con questa semantica possiamo ridurre il tempo utilizzato nella creazione di applicazioni in odo che la manutenzione sia meno intricata, grazie al fatto che ogni funzione sarà responsabile solo di una azione; in questo modo possiamo sapere esattamente dove vengono generati gli errori.

Finalmente questo approccio concede al programmatore una nuovo metodo per risolvere i compiti: la separazione di un problema complesso in piccole parti.

 

A proposito di me

Diego Phares

Programmatore che si svolge nel mondo front-end e back-end. Vanilla JS <3. Innamorato dei linguaggi umani e informatici.

Di Diego Phares

Gli articoli più letti

Articoli recenti

Commenti recenti