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.
Il seguente codice offre un esempio di soluzione a tale obiettivo:
function compose(f1, f2) {
let composedFunction = function composedFunction(...args) {
return f1(f2(...args));
};
return composedFunction;
}
Come possiamo vedere, 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;
}
}
Come possiamo vedere, abbiamo sfruttato pesantemente l'operatore spread e i rest parameter per ottenere una composizione dinamica delle funzioni passate come parametro. La funzione restituita inverte l'elenco delle funzioni ricevuto in input, applica la prima funzione ai parametri passati ed itera l'elenco delle funzioni applicandole una dopo l'altra al risultato ottenuto in precedenza.
La possibilità di prendere in input una o più funzioni e di manipolarle come se fossero dei normali dati ci offre una enorme opportunità non solo per la loro composizione dinamica, ma anche per l'applicazione di design pattern come ad esempio decorator. Tra l'altro, anche questo pattern da un punto di vista funzionale non è altro che una specifica composizione.