Il concetto di chiusura non è specifico per la programmazione funzionale. Significa semplicemente che hai:
- Una variabile in un ambito
- Qualcosa costruito all'interno di questo ambito (in genere una funzione o un oggetto) che utilizza detta variabile
- Che qualcosa viene passato a qualche parte fuori dall'ambito di origine
L'esempio da manuale è qualcosa del tipo:
function foo() {
var x = 23;
var bar = function(y) {
return x + y;
};
return bar;
}
... e diciamo che bar
chiude oltre x
.
Tuttavia, possiamo fare lo stesso con gli oggetti, in realtà:
function foo() {
var x = 23;
var bar = {
"y": x
};
return bar;
}
Il meccanismo è lo stesso, anche se ora non è una funzione che si chiude su x
, ma su un oggetto. Di solito non si chiama chiusura, perché a differenza delle chiusure di funzioni, questo tipo di comportamento è "ovvio" per un programmatore imperativo, e l'idea è che stiamo semplicemente facendo riferimento a una variabile locale per mettere un valore in un oggetto - ma quindi, è esattamente ciò che fa una chiusura, solo che l'oggetto può anche essere una funzione.
Ora, per quanto riguarda la programmazione funzionale: il concetto più importante è la funzione . A differenza delle funzioni in linguaggi imperativi, per i quali "routine" o "procedura" è in realtà un nome molto migliore, le funzioni di programmazione funzionale sono concettualmente simili alle funzioni matematiche: mappature dalle cose alle cose. Prendendo questo concetto e utilizzandolo come la primitiva espressiva prima e più importante, gli altri segni distintivi della programmazione funzionale seguono logicamente:
- Funzioni mappano gli input alle uscite; questo è tutto che fanno. Gli effetti collaterali come la stampa o il mantenimento dello stato mutabile non si adattano a questo modello, quindi i programmatori funzionali tendono ad evitare queste cose. Questo è ciò che la gente chiama la purezza : una funzione pura è una funzione che non ha effetti collaterali. Diverse lingue FP trattano questa materia in modo diverso; all'estremo opposto, c'è Haskell, che non consente affatto alcuna funzione impura, mentre alla fine pragmatica ci sono Lisp, Scheme, JavaScript ecc., che permettono agli effetti collaterali di apparire ovunque e lasciano il loro elusione ai programmatori di boyscout.
- Anche le funzioni sono cose, quindi ha senso avere funzioni che prendono funzioni come input, o restituiscono funzioni come output, o entrambe. Tali funzioni sono note come funzioni di ordine superiore e il loro uso è onnipresente nella programmazione funzionale. La famosa funzione
map
è una funzione di ordine superiore: uno dei suoi argomenti è una funzione, che map
si applica a ogni elemento nell'altro argomento (che è presumibilmente un elenco di qualche tipo).
- Le funzioni possono essere espresse in termini di se stessi, a.k.a. ricorsione . Le definizioni ricorsive sono più facili da scrivere in modo puro; non si basano su costrutti di stato mutabile come le variabili del contatore di loop. Per questo motivo, i programmatori funzionali tendono a preferire soluzioni ricorsive rispetto a quelle iterative e i linguaggi di programmazione funzionale forniscono varie ottimizzazioni per evitare i problemi che possono causare la programmazione ricorsiva (overflow dello stack, perdite di memoria, ecc.). Inoltre, tipi comuni di ricorsione sono disponibili in modo generico in ogni linguaggio di programmazione funzionale che vale l'etichetta, i più famosi sono
map
e reduce
(a.k.a. fold
). Con l'aiuto di queste funzioni, un programmatore funzionale può astrarre i dettagli della ricorsione effettiva e pensare in termini di map
, reduce
e filter
diventa una seconda natura ad un certo punto.
- Il tipo di funzione preferito è la funzione unario , che accetta un solo argomento. Usando chiusure, qualsiasi funzione n-ario può essere riscritta come una funzione unaria che restituisce una funzione (n-1) -aria, e applicando completamente questa logica, qualsiasi funzione n-ario può essere scritta come una catena annidata di funzioni unarie che ritornano il prossimo link della catena. Questo processo è chiamato currying , mentre il concetto di chiamare una funzione con un insieme incompleto di argomenti, che fornisce un'altra funzione che prende il resto degli argomenti, è noto come applicazione di funzione parziale . Come un semplice esempio, se hai una funzione che puoi chiamare in questo modo:
foo(a, b, c)
, la versione completa sarà chiamata come: foo(a)(b)(c)
; chiamare il foo al curry in questo modo: foo(a)(b)
costituisce un'applicazione parziale e produce una funzione che, quando chiamata con c
, dà lo stesso risultato della chiamata originale, ad esempio f = foo(a)(b); f(c)
.