Il currying è una tecnica avanzata che si applica alle funzioni. Non viene utilizzata solamente in JavaScript, ma anche in altri linguaggi di programmazione.
Il currying è una trasformazione che traduce una funzione invocabile come f(a, b, c)
in una invocabile come f(a)(b)(c)
.
Il currying non invoca la funzione. Si occupa solamente della sua trasformazione.
Come prima cosa vediamo un esempio, in modo da capire di cosa stiamo parlando, e le applicazioni nella pratica.
Creeremo una funzione di supporto curry(f)
che esegue il currying per una funzione a due argomenti f
. In altre parole, curry(f)
, trasformerà f(a, b)
in una funzione invocabile come f(a)(b)
:
function curry(f) { // curry(f) esegue il currying
return function(a) {
return function(b) {
return f(a, b);
};
};
}
// utilizzo
function sum(a, b) {
return a + b;
}
let curriedSum = curry(sum);
alert( curriedSum(1)(2) ); // 3
Come potete vedere, l’implementazione è piuttosto semplice: sono due semplici wrappers.
- Il risultato di
curry(func)
è un wrapperfunction(a)
. - Quando viene invocato come
curriedSum(1)
, l’argomento viene memorizzato nel Lexical Environment, e viene ritornato un nuovo wrapperfunction(b)
. - Successivamente questo wrapper viene invocato con
2
come argomento, che passerà l’invocazione asum
.
Implementazioni più avanzate del currying, come _.curry fornito dalla libreria lodash, ritorna un wrapper che consente di invocare una funzione sia nella forma standard che in quella parziale:
function sum(a, b) {
return a + b;
}
let curriedSum = _.curry(sum); // utilizzando _.curry della libreria lodash
alert( curriedSum(1, 2) ); // 3, riamane invocabile normalmente
alert( curriedSum(1)(2) ); // 3, invocata parzialmente
Currying? Per quale motivo?
Per poterne comprendere i benefici abbiamo bisogno di un esempio di applicazione reale.
Ad esempio, abbiamo una funzione di logging log(date, importance, message)
che formatta e ritorna le informazioni. In un progetto reale, una funzione del genere ha diverse funzionalità utili, come l’invio di log in rete; qui useremo semplicemente un alert
:
function log(date, importance, message) {
alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}
Eseguiamo il currying!
log = _.curry(log);
Successivamente al log
, funzionerà normalmente:
log(new Date(), "DEBUG", "some debug"); // log(a, b, c)
…Ma funzionerà anche nella forma parziale:
log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)
Ora possiamo creare una funzione utile per registrare i logs:
// logNow sarà la versione parziale di log con il primo argomento fisso
let logNow = log(new Date());
// utilizziamola
logNow("INFO", "message"); // [HH:mm] INFO message
Ora logNow
equivale a log
con il primo argomento fissato, in altre parole, una “funzione applicata parzialmente” o “parziale” (più breve).
Possiamo anche andare oltre, e creare una funzione utile per registrare i logs di debug:
let debugNow = logNow("DEBUG");
debugNow("message"); // [HH:mm] DEBUG message
Quindi:
- Non perdiamo nulla dopo il currying:
log
rimane invocabile normalmente. - Possiamo generare molto semplicemente funzioni parziali per i logs quotidiani.
Implementazione avanzata del currying
Nel caso in cui vogliate entrare più nel dettaglio, di seguito vediamo un’implementazione “avanzata” del currying per funzioni con più argomenti, che avremmo anche potuto usare sopra.
E’ piuttosto breve:
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
Esempi di utilizzo:
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
alert( curriedSum(1, 2, 3) ); // 6, ancora invocabile normalmente
alert( curriedSum(1)(2,3) ); // 6, currying del primo argomento
alert( curriedSum(1)(2)(3) ); // 6, currying completo
La funzione curry
può sembrare complicata, ma in realtà è piuttosto semplice da capire.
Il risultato dell’invocazione curry(func)
è il wrapper curried
(che ha subito il processo di currying), ed appare in questo modo:
// func è la funzione trasformata
function curried(...args) {
if (args.length >= func.length) { // (1)
return func.apply(this, args);
} else {
return function(...args2) { // (2)
return curried.apply(this, args.concat(args2));
}
}
};
Quando la eseguiamo, ci sono due percorsi di esecuzione if
:
- Se il numero di
args
forniti è uguale o maggiore rispetto a quelli che la funzione originale ha nella sua definizione (func.length
), allora gli giriamo semplicemente l’invocazione utilizzandofunc.apply
. - Altrimenti, otterremo un parziale: non invochiamo ancora
func
. Invece, viene ritornato un altro wrapper, che riapplicherà ilcurried
passando gli argomenti precedenti insieme a quelli nuovi.
Quindi, se la invochiamo, di nuovo, avremo o una nuova funzione parziale (se non vengono forniti abbastanza argomenti) oppure otterremo il risultato.
Il currying richiede che la funzione abbia un numero fissato di argomenti.
Una funzione che utilizza i parametri rest, come f(...args)
, non può passare attraverso il processo di currying in questo modo.
Per definizione, il currying dovrebbe convertire sum(a, b, c)
in sum(a)(b)(c)
.
Ma la maggior parte delle implementazioni in JavaScript sono più avanzate di così, come descritto: queste mantengono la funzione invocabile nella variante a più argomenti.
Riepilogo
Il currying è una trasformazione che rende f(a,b,c)
invocabile come f(a)(b)(c)
. Le implementazioni in JavaScript, solitamente, mantengono entrambe le varianti, sia quella normale che quella parziale, se il numero di argomenti non è sufficiente.
Il currying permette di ottenere delle funzioni parziali molto semplicemente. Come abbiamo visto nell’esempio del logging, dopo il currying la funzione universale a tre argomenti log(date, importance, message)
ci fornisce una funzione parziale quando invocata con un solo argomento (come log(date)
) o due argomenti (come log(date, importance)
).
Commenti
<code>
, per molte righe – includile nel tag<pre>
, per più di 10 righe – utilizza una sandbox (plnkr, jsbin, codepen…)