La programmazione funzionale

esecuzione del programma assume la forma di una serie di valutazioni di funzioni
matematiche.

Il punto di forza principale di questo paradigma è la mancanza di effetti collaterali (side-effect) delle funzioni, il che comporta una più facile verifica della correttezza e della mancanza di bug del programma e la possibilità di una maggiore ottimizzazione dello stesso. Un uso particolare del paradigma, per l’ottimizzazione dei programmi, è quello di trasformare gli stessi per utilizzarli nella programmazione parallela.

La programmazione funzionale pone maggior accento sulla definizione di funzioni, rispetto ai paradigmi procedurali e imperativi, che invece prediligono la specifica di una sequenza di comandi da eseguire. In questi ultimi, i valori vengono calcolati cambiando lo stato del programma attraverso delle assegnazioni; un programma funzionale, invece,
è immutabile: i valori non vengono trovati cambiando lo stato del programma, ma costruendo nuovi stati a partire dai precedenti.

Le funzioni matematiche sono molto potenti in termini di flessibilità ed analisi.
Per esempio se una funzione è idempotente e non ha effetti collaterali, allora una chiamata
a questa funzione che ha il suo stesso output come input (f(f(x))) può essere efficientemente computata con una singola chiamata alla funzione.
Questo tipo di funzioni hanno zero o più parametri e un singolo valore di ritorno. I parametri sono gli input della funzione ed il valore di ritorno è il suo output.
La definizione di una funzione descrive come questa debba essere valutata (computata) in termini di altre funzioni. Per esempio, la funzione f(x) = x2 + 2 è definita in termini
delle funzioni di potenza e addizione. Ad un certo punto della serie di chiamate a funzioni, il linguaggio metterà a disposizione funzioni atomiche, cioè funzioni che non
richiamano nessun’altra funzione.

Da un punto di vista puramente teorico, la programmazione funzionale prende il concetto di funzione matematica come elemento di base del calcolo.


Una funzione matematica è una relazione tra insiemi che associa ad uno o più elementi di un insieme, detto dominio, uno e un solo elemento di un altro insieme, detto codominio.

In un linguaggio di programmazione funzionale, le funzioni possono essere manipolate in diversi modi. Solitamente, in questi linguaggi, le funzioni sono entità di prima classe,
cioè possono essere passate come parametri ad altre funzioni e possono essere restituite come valori di ritorno da altre funzioni. Ciò permette a funzioni come mapcar in Lisp
e map in Haskell, che prendono una funzione e una lista come input ed applicano la funzione in input ad ogni elemento della lista, di restituire una lista dei risultati.

Le funzioni possono avere dei nomi, come in ogni altro linguaggio, o essere definite anonimamente (a volte anche a run-time) usando l’astrazione lambda, ed essere usate come
valori in altre funzioni.
Questo è uno dei vantaggi fondamentali della programmazione funzionale rispetto alla programmazione imperativa. Infatti, nella programmazione tradizionale l’applicazione di una funzione può in certi casi restituire risultati diversi a parità di valori di input. Questo accade quando la funzione fa riferimento ad uno stato, condiviso o meno, e questo viene
modificato da effetti collaterali derivanti dall’esecuzione della funzione stessa o di altre entità. Avremo modo di tornare su questi concetti nel corso della guida.
Per il momento ci basta evidenziare i seguenti punti caratterizzanti della programmazione funzionale:
le funzioni sono funzioni matematiche che per un dato input restituiscono sempre lo stesso risultato;
le funzioni non modificano i dati ricevuti in input, ma restituiscono sempre nuovi valori;
le funzioni possono essere passate come parametro e restituite da altre funzioni, e possono essere combinate tra di loro.

È importante evidenziare che la programmazione funzionale è un paradigma di programmazione, cioè un modo di vedere la programmazione, un approccio, un modello per la costruzione di programmi. Come tale quindi non è legato ad uno specifico linguaggio di programmazione. Naturalmente, un linguaggio di programmazione specificamente progettato per la programmazione funzionale ci indirizza all’applicazione dei principi di questo paradigma e ci impedisce di commettere errori grossolani che violano i principi di base. Con un linguaggio non funzionale dobbiamo essere noi ad imporci una disciplina nella scrittura di codice che rispetti i principi di base del paradigma funzionale. E questo non sempre risulta così semplice, soprattutto se non è chiaro il modello proposto e se continuiamo a pensare con un approccio imperativo.


Nell’ambito della programmazione funzionale si fa una chiara distinzione tra funzioni pure e funzioni non pure. Le prime sono quelle funzioni che hanno un comportamento matematico,
cioè sono quelle funzioni che dato un determinato input restituiscono sempre lo stesso output e non hanno effetti collaterali. Le seconde sono le funzioni che invece producono effetti
collaterali, come la funzione add() vista prima.

Quindi, per adottare un approccio funzionale nella scrittura del nostro codice JavaScript dobbiamo fare in modo che le nostre funzioni siano deterministiche come le funzioni matematiche e fare attenzione ad evitare effetti collaterali durante la sua esecuzione.

Come conseguenza della definizione di funzione matematica possiamo affermare che:


Una funzione senza parametri non è pura o non ha senso

Consideriamo infatti una funzione come la seguente:

function getDate() {
return new Date();
}

Dal momento che una funzione è una relazione tra dati di due insiemi, una funzione che non prevede nessun parametro non sta mettendo in relazione nulla. Inoltre, se la funzione restituisce dei valori diversi ad ogni chiamata, è segno che sta facendo riferimento ad un contesto esterno. È questo il caso del nostro esempio: la funzione non prende nessun valore in input e restituisce valori diversi ad ogni invocazione. I valori restituiti dipendono effettivamente dal sistema su cui è eseguita la funzione, quindi essa non è assolutamente una funzione pura.


Una funzione senza un output non è pura

Anche in questo caso, una funzione che prende dei valori in input e non restituisce un output non sta mettendo in relazione gli elementi di due insiemi, e molto probabilmente sta
soltanto generando effetti collaterali, come può essere ad esempio la seguente funzione:

var value = 0;
function add(n) {
value = value + n;
}

In questo caso la funzione non restituisce esplicitamente un valore, ma sta modificando il valore di una variabile globale e pertanto non è pura.


La capacità di restituire sempre lo stesso output in corrispondenza di un dato input fornisce una caratteristica interessante: la trasparenza referenziale.
Il fatto di poter trascurare il contesto in cui viene eseguita una funzione pura, consente di poter sostituire la valutazione di una specifica chiamata di funzione con il suo risultato senza alterare la semantica dell’applicazione. Ad esempio, se abbiamo la seguente funzione pura:

function somma(x, y) {
return x + y;
}

e la seguente espressione:

var risultato = somma(3, 5) * 2;

possiamo tranquillamente sostituire alla chiamata somma(3, 5) il suo risultato 8 ed essere sicuri di non aver introdotto malfunzionamenti nel comportamento dell’applicazione:

var risultato = 8*2;

Questa osservazione può sembrare banale, ma non è scontata in programmi scritti con un approccio non funzionale. Tecnicamente diciamo che le funzioni pure garantiscono
la trasparenza referenziale, cioè la capacità di sostituire una espressione con il suo risultato, mantenendo la correttezza dell’applicazione.


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


Una delle peculiarità della programmazione funzionale è rappresentata dal fatto che una funzione può essere considerata come un dato, e pertanto può essere passata come parametro
o restituita come risultato, così come facciamo con numeri, stringhe, array, ecc. Una funzione che accetta altre funzioni come parametro o restituisce una funzione come risultato
viene chiamata funzione di ordine superiore o high-order function.

Naturalmente questa non è una novità per JavaScript, ed è questo uno dei motivi per cui, in linea di massima, il linguaggio si presta abbastanza bene all’adozione del paradigma
funzionale.

Per comprendere meglio le potenzialità di una funzione di ordine superiore, proviamo a scriverne una che ci consenta di realizzare la composizione di funzioni. Vogliamo cioè
definire una funzione che, date due funzioni in input, restituisca la funzione risultante dalla loro composizione.


Se paragonata alla programmazione imperativa, può sembrare che la programmazione funzionale manchi di molti costrutti spesso (ma incorrettamente) ritenuti essenziali
per un linguaggio imperativo, come il C o il Pascal. Per esempio, nella programmazione funzionale rigorosa, non c’è alcuna esplicita allocazione di memoria o assegnazione
di variabile, ma, comunque, queste operazioni avvengono automaticamente quando una funzione è invocata: l’allocazione di memoria avviene per creare lo spazio necessario per
i parametri e il valore di ritorno e l’assegnazione avviene per copiare i parametri nel nuovo spazio allocato e per copiare il valore di ritorno alla funzione chiamante.

Entrambe le operazioni possono avvenire solo alla chiamata di una funzione e al ritorno da essa e quindi gli effetti collaterali sono eliminati. Eliminando gli effetti
collaterali dalle funzioni, si ottiene ciò che viene chiamata trasparenza referenziale, che assicura che il risultato di una funzione sia lo stesso per uno stesso insieme
di parametri, indifferentemente da quando e dove questa funzione venga valutata. La trasparenza referenziale rende molto più facile sia la dimostrazione della correttezza
del programma sia l’identificazione automatica delle computazioni indipendenti per l’esecuzione parallela.

Le iterazioni, un altro costrutto della programmazione imperativa, sono ottenute attraverso il costrutto più generale delle chiamate ricorsive a funzioni. Le funzioni ricorsive
invocano se stesse permettendo di eseguire più e più volte una stessa operazione. Può essere dimostrato che un’iterazione è equivalente ad uno speciale tipo di ricorsione
chiamata tail recursion. La ricorsione nella programmazione funzionale può assumere molte forme e, in generale, è una tecnica più potente dell’iterazione. Per questa ragione,
quasi tutti i linguaggi imperativi la supportano (con la notevole eccezione del Fortran 77 e del COBOL, prima del 2002).

La funzione compose() prende in input due parametri che rappresentano le due funzioni da comporre. Essa definisce e restituisce la funzione composedFunction()
che prende gli eventuali argomenti passati e li passa alla composizione delle due funzioni, come abbiamo visto quando abbiamo parlato di questo argomento. Si noti come nel
passaggio dei parametri dalla funzione composta alla prima delle funzioni da comporre abbiamo utilizzato la notazione rest parameter e l’operatore spread, entrambi rappresentati dai
puntini di
sospensione, molto comodi nella gestione di un numero indefinito di elementi.

La funzione compose() è quindi una funzione di ordine superiore che ci consente di manipolare altre funzioni. Proponiamo un esempio specifico di utilizzo della funzione
compose() per chiarire come utilizzarla:

function split(string) {
return string.split(” “);
}
function count(array) {
return array.length;
}
var countWords = compose(count, split);
console.log(countWords(“funzione di ordine superiore”));
//risultato: 4

Naturalmente la funzione compose(), per come l’abbiamo definita, soffre della limitazione di poter comporre soltanto due funzioni.
In realtà questo non è un grosso problema, perché possiamo sempre comporre la funzione ottenuta con una nuova funzione riutilizzando la stessa funzione compose(),
come mostrato dal seguente esempio:

function split(string) {
return string.split(” “);
}
function reverse(array) {
return array.reverse();
}
function join(array) {
return array.join()
}
var reverseString = compose(join, compose(reverse, split));

In questo caso le tre funzioni vengono composte incrementalmente. Ma se vogliamo una funzione generica che prende una lista indefinita di funzioni e le compone in una nuova funzione possiamo utilizzare la seguente:

function compose(…funcs) {
return function composedFunction(…args) {
let [f1, …funcsList] = funcs.reverse();
let result = f1(…args);
for (let func of funcsList) {
result = func(result);
}
return result;
}
}


Si può immaginare un array come una sorta di contenitore, le cui caselle sono dette celle (o elementi) dell’array stesso. Ciascuna delle celle si comporta come una variabile
tradizionale; tutte le celle sono variabili di uno stesso tipo preesistente, detto tipo base dell’array. Si parlerà perciò di tipi come “array di interi”, “array di stringhe”,
“array di caratteri” e così via. Quello che si ottiene dichiarandolo è dunque un contenitore statico ed omogeneo di valori, variabili o oggetti. In alcuni linguaggi, la dimensione
dell’array (ovvero il numero celle di cui esso è composto) viene considerato parte della definizione del tipo array; in tal caso, si parlerà più precisamente di tipi come “array
di 100 caratteri” o “array di 10 interi”.
Ciascuna delle celle dell’array è identificata da un valore di indice. L’indice è generalmente numerico e i valori che gli indici possono assumere sono numeri interi contigui
che partono da 0[2] o da 1[3] o, più raramente, da un valore arbitrario, come in Pascal. Si potrà quindi parlare della cella di indice 0, di indice 1, e, in generale, di indice N,
dove N è un intero compreso fra 0 (o 1) e il valore massimo per gli indici dell’array.
La possibilità di accedere agli elementi attraverso un indice è la principale caratteristica di un array. È possibile accedere singolarmente ad una sua generica posizione
(“accesso casuale”, come per la memoria), oltre a scorrerlo sequenzialmente in entrambe le direzioni tramite un ciclo iterativo in tutti i suoi elementi o a partire da alcuni di
essi.
La maggior parte dei programmi che utilizzano array si servono della struttura di controllo “cicli for” per attraversare gli array, ovvero per accedere sequenzialmente alle celle.
Il ciclo for si presta molto naturalmente all’uso combinato dell’array proprio perché consente di specificare un tipo particolare di iterazione di un certo insieme di istruzioni,
controllato da un indice che assume un insieme di valori solitamente interi e contigui. Per esempio, il codice Java per stampare i contenuti di un array di n interi potrebbe
essere come segue:

for(int i=0; imax)
max = vettore[i];
if(vettore[i]<min)
min = vettore[i];
somma += vettore[i];
}
media = ((double)somma)/vettore.length;
System.out.println(“Minimo: “+min);
System.out.println(“Massimo: “+max);
System.out.println(“Media: “+media);
}
else{
System.out.println(“Il vettore non contiene alcun elemento”);}


I programmi puramente funzionali (cioè quelli che obbligano a rispettare le regole del non-effetto-collaterale, trasparenza referenziale, ecc., a differenza dei linguaggi
non puramente funzionali che sono un ibrido tra paradigma funzionale e imperativo) non richiedono variabili e non hanno effetti collaterali e sono di conseguenza automaticamente
thread-safe. Solitamente i linguaggi funzionali fanno un uso piuttosto sofisticato dello stack.

Il nome di questa struttura dati è infatti la stessa parola inglese usata, per esempio,
per indicare una “pila di piatti” o una “pila di giornali”, e sottende per l’appunto l’idea che quando si pone un piatto nella pila lo si metta in cima, e che quando si preleva
un piatto si prelevi, analogamente, quello in cima (da cui la dinamica LIFO), anche se è possibile inserire o prelevare elementi dalla coda; più in generale, la pila è un
particolare tipo di lista in cui le operazioni di inserimento ed estrazione si compiono dallo stesso estremo.

Una tecnica molto comune nell’ambito della programmazione funzionale è l’applicazione parziale di una funzione.
Per applicazione parziale intendiamo l’applicazione di una funzione che accetta n argomenti, ad un numero di argomenti inferiore, ottenendo così una nuova funzione specializzata.

Nell’ambito funzionale, il numero di argomenti di una funzione è detto arietà (arity).
Quindi, da un punto di vista formale, possiamo dire che l’applicazione parziale è una tecnica che consente di ridurre l’arietà di una funzione.

Proviamo a spiegare questa tecnica con un esempio. Supponiamo di avere la funzione somma() descritta dal seguente codice:

function somma(x, y) {
return x + y;
}

Come risulta evidente, questa funzione prevede due argomenti e restituisce la loro somma. Consideriamo ora la seguente definizione di funzione:

function incrementa(x) {
return somma(1, x);
}

La funzione incrementa() è il risultato dell’applicazione parziale della funzione somma(). In altre parole, la funzione incrementa() è una funzione specializzata nella somma di 1 al suo secondo parametro. All’atto pratico, la funzione incrementa() equivale alla funzione somma() con un numero ridotto di argomenti, dal momento che il primo viene fissato a 1.


I linguaggi funzionali permettono inoltre una tecnica chiamata currying, che permette di trasformare una funzione con parametri multipli in una funzione con un solo parametro
che mappa ad un’altra funzione con un solo parametro e così via, fino all’esaurimento dei parametri. La funzione trasformata può essere applicata ad un sottoinsieme dei suoi
parametri iniziali e dare come risultato una nuova funzione dove i parametri di quel sottoinsieme sono costanti e il resto dei parametri hanno valori non ancora specificati.

Infine questa nuova funzione può essere applicata ai parametri rimanenti per ottenere il risultato finale. Per esempio, una funzione somma(x,y) = x + y può essere trasformata
in modo tale che il valore di ritorno di somma(2) (nota che non c’è il parametro y) sia una funzione anonima equivalente alla funzione somma2(y) = 2 + y. Questa nuova funzione
ha un solo parametro a cui somma 2. Questa tecnica non sarebbe possibile se le funzioni non fossero entità di prima classe.

Il currying è una tecnica che consente di decomporre una funzione di arietà n in una sequenza di n funzioni di un solo argomento.
Consideriamo ancora una volta la funzione somma() vista nelle lezioni precedenti per spiegare meglio il concetto:

function somma(x, y) {
return x + y;
}

Applicando il currying otteniamo la seguente definizione:

function sommaCurried(x) {
return function(y) {
return x + y;
};
}

Sfruttando le arrow function, possiamo definire la stessa funzione in maniera più compatta come mostrato di seguito:

var sommaCurried = x => y => x + y;

Indipendentemente dalla forma con cui abbiamo definito la funzione sommaCurried(), il risultato consiste in una funzione che restituisce un’altra funzione,
che a sua volta restituisce il calcolo effettivo. Ciascuna delle funzioni restituite ha un solo parametro. Possiamo infatti invocare la somma di due numeri come mostrato dal seguente codice:

var totale=somma Curried(2)(4);

Abbiamo già introdotto la necessità di ripensare i cicli quando si utilizza Javascript nel contesto della programmazione funzionale.
Oltre a forEach(), JavaScript ci mette a disposizione anche una serie di metodi per elaborare array con un approccio funzionale.
Il primo di questi metodi è filter(), che restituisce un nuovo array con gli elementi individuati dalla funzione booleana passata come parametro.

Consideriamo il seguente esempio:

var libriDiSalgari = libri.filter(function(libro) {
return (libro.autore == “Emilio Salgari”);
});

Come possiamo vedere, la funzione passata al metodo filter() restituisce semplicemente la condizione booleana che deve essere verificata per individuare gli elementi dell’array
libri da inserire nel nuovo array risultante. Questo approccio ci evita di ricorrere a cicli e ad effetti collaterali per ottenere il risultato atteso.
Tra l’altro il codice è più compatto e leggibile, soprattutto se utilizziamo le arrow function, come nel seguente esempio:

var libriDiSalgari = libri.filter(libro => libro.autore == “Emilio Salgari”);

Qualche lettore potrebbe obiettare che il metodo filter() non sia una funzione nel vero senso della parola, in quanto ha un solo parametro (la funzione di ordine superiore che determina la condizione di filtro) ma è come se avesse un secondo parametro implicito (l’array degli elementi da filtrare).
In effetti è così: il metodo filter(), come altri metodi che vedremo, prende come parametro implicito l’array di cui è metodo.
Questo è dovuto alla natura ad oggetti degli array JavaScript.

Tuttavia questo non inficia la natura funzionale del metodo, che non modifica i suoi parametri, nemmeno quello implicito, e restituisce un nuovo valore.
Tra l’altro, l’esecuzione di filter() sullo stesso array e con la stessa funzione passata come parametro restituisce sempre lo stesso risultato.
Possiamo quindi considerare la presenza di questo parametro implicito come una sorta di notazione sintattica speciale che mette d’accordo mondo funzionale e mondo ad oggetti.

Oltre a filter(), uno dei metodi funzionali più utili è il metodo map().
Questo metodo accetta come parametro una funzione e restituisce un nuovo array i cui elementi sono costituiti dai valori restituiti dalla funzione.
Consideriamo il seguente esempio:

var titoli = libri.map(libro => libro.titolo);

La variabile titoli conterrà un array con l’elenco dei titoli estratti dall’array libri. Da notare la compattezza e la semplicità con cui otteniamo questo risultato.