Nella lezione precedente, abbiamo 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.
Dal momento che questi metodi restituiscono un nuovo array, possiamo
comporli per ottenere risultati più sofisticati. Prendiamo ad esempio in esame
questo codice:
var titoliDiSalgari = libri.filter(libro => libro.autore == "Emilio Salgari").map(libro => libro.titolo);
Abbiamo eseguito i metodi filter()
e map()
per ottenere in maniera elegante un array dei soli titoli dei libri di
Emilio Salgari.
Un altro metodo funzionale degli array, spesso poco compreso, è reduce()
. Esso accetta come parametro una funzione e
restituisce un valore determinato dalla funzione stessa. A tale funzione
vengono passati due parametri: un accumulatore ed un elemento dell'array.
L'accumulatore è un valore risultante dall'applicazione della funzione
nella precedente iterazione. Proviamo a fare un esempio per spiegare meglio
il funzionamento di reduce()
. Supponiamo di voler individuare il
libro che ha il titolo più lungo. Questo codice fa al caso nostro:
var libroDalTitoloPiuLungo = libri.reduce((acc, libro) => acc.titolo.length > libro.titolo.length? acc: libro);
Se non vogliamo utilizzare le arrow function nè le espressioni condizionali possiamo ottenere lo stesso risultato così:
var libroDalTitoloPiuLungo = libri.reduce(function(acc, libro) {
let result;
if (acc.titolo.length > libro.titolo.length) {
result = acc;
} else {
result = libro;
}
return result;
});
Indipendentemente dalla notazione sintattica scelta, la funzione passata a reduce()
otterrà il primo ed il secondo elemento dell'array alla
prima esecuzione, mentre le volte successive otterrà come primo parametro
il valore restituito dalla precedente elaborazione. Nel nostro caso, la
funzione avrà come valore del parametro acc il libro con il titolo
più lungo fino a quel momento e potrà così confrontarlo con il libro
corrente per stabilire se il libro da individuare rimane sempre acc o meno.
Combinando i tre metodi map()
, filter()
e reduce()
, è possibile effettuare elaborazioni anche complesse sugli array mantenendo un approccio strettamente funzionale.
Il seguente esempio rimuove i duplicati in un array di numeri combinando reduce()
e filter()
:
var numeri = [1, 2, 1, 6, 3, 5, 7, 4, 5, 3, 4, 4, 2, 4, 6];
var result = numeri.reduce((acc, current) => {
const length = acc.length
if (length == 0 || acc.filter(x => x == current).length == 0) {
acc.push(current);
}
return acc;
}, []);
In questo caso notiamo che il metodo
reduce()
è invocato passando un array vuoto come secondo parametro. Esso rappresenta
il valore iniziale da assegnare all'accumulatore. Passando questo valore,
quindi, la prima invocazione della funzione avrà un array vuoto come primo
parametro e il primo elemento dell'array come secondo parametro invece dei
primi due elementi dell'array.