Nei linguaggi di programmazione, una chiusura è una astrazione che combina una funzione con le variabili libere presenti nell'ambiente in cui è definita secondo le regole di scope del linguaggio. Le variabili libere dell'ambiente rimangono accessibili per tutta la durata di vita (extent) della chiusura e pertanto persistono nel corso di invocazioni successive della chiusura. Di conseguenza, le variabili della chiusura possono essere usate per mantenere uno stato ed emulare costrutti della programmazione a oggetti.
Le chiusure si rivelano utili quando una funzione ha bisogno di "ricordare" informazioni: ad esempio un parametro specifico per un'operazione di confronto, oppure il riferimento ad un widget in un callback di un'interfaccia grafica.
Teoria e implementazione
[modifica | modifica wikitesto]Le chiusure, tipicamente, sono implementate con una speciale struttura dati che contiene un puntatore al codice della funzione ed una rappresentazione dell'ambiente lessicale della stessa al momento della sua creazione (per esempio l'insieme delle variabili disponibili e dei relativi valori).
L'implementazione delle variabili libere legate in una chiusura richiede un trattamento diverso dalle normali variabili che molti linguaggi mantengono su uno stack lineare. Infatti lo stack viene liberato quando si ritorna da una invocazione, mentre le variabili libere di una chiusura devono sopravvivere. Pertanto tali variabili devono essere allocate diversamente in modo da persistere finché non siano più utilizzabili. Di solito quindi le variabili della chiusura sono allocate nello heap e si fa ricorso alla garbage collection per deallocare la chiusura. Questo spiega perché le chiusure sono tipicamente presenti in linguaggi dotati di garbage collector.
Il concetto di chiusura può essere collegato a quello di attori del modello ad attori nella computazione concorrente, dove i valori delle variabili nell'ambiente lessicale della funzione sono chiamati conoscenti dell'attore. Un'importante questione sulle chiusure nei linguaggi a paradigma concorrente è se le variabili di una chiusura possono essere aggiornate e, in tal caso, come possono essere sincronizzati questi cambiamenti. Gli attori sono una soluzione a questo problema[1].
Chiusure e funzioni
[modifica | modifica wikitesto]Le chiusure sono presenti in quei linguaggi che trattano le funzioni come oggetti di prima classe (first-class object), cioè consentono di passarle come parametri ad altre funzioni, restituirle come valori da altre funzioni, assegnarle a variabili, ecc., come si fa con i tipi più semplici, quali stringhe, interi, ecc..
In ML, il seguente codice definisce una funzione f
che restituisce il parametro passatogli più 1:
fun f(x) = x + 1;
Se una funzione utilizza variabili libere, ossia non locali, al momento della sua definizione si crea una chiusura che cattura tali variabili. Per esempio, in questo frammento di codice:
val y = 1;
fun f(x) = x + y;
la struttura dati chiusura che rappresenta f
contiene un riferimento alla variabile y
presente nell'ambiente di definizione, in cui è legata al valore 1.
Poiché in ML il valore di y
non può cambiare il comportamento di f
non cambia.
Invece se si usa una variabile assegnabile, f
restituisce il valore del suo parametro sommato al valore di y
al momento in cui viene chiamata.
Ad esempio:
val y = ref 1;
let f(x) = x + !y;
f(3);
y := 5;
f(3);
La prima chiamata di f 3
restituisce 4, la seconda invece 8.
Per chiarire il ruolo dell'ambiente lessicale, si consideri il seguente frammento in ML:
let
val y = 1;
fun f(x) = x + y;
in
let
val y = 2;
in
f(3)
end
end
in questo esempio esistono due diverse variabili con lo stesso nome y
, quella del let
più esterno, in cui vale 1, e quella del let
più interno, in cui vale 2.
La chiusura f
viene creata nell'ambiente più esterno, e fa quindi riferimento alla y
con valore 1 e per questo, il risultato che si ottiene è 4, non 5.
Utilizzi delle chiusure
[modifica | modifica wikitesto]Le chiusure hanno molti utilizzi:
- Possono essere usate per aumentare l'astrazione e la versatilità di alcune funzioni. Per esempio, una funzione per l'ordinamento di una lista di valori può accettare una chiusura come parametro, la quale confronta due valori da ordinare secondo un criterio definito dall'utente.
- Dato che una chiusura posticipa la sua valutazione (in inglese, evaluation) (cioè, non "fa" niente fin quando non viene invocata), può essere utilizzata per definire strutture di controllo. Per esempio, tutte le strutture di controllo standard dello Smalltalk, incluse quelle condizionali (if/then/else) e i cicli (while e for), sono definite usando oggetti i cui metodi accettano delle chiusure. I programmatori, quindi, possono anch'essi scrivere delle nuove strutture di controllo.
- Possono essere realizzate più funzioni che "racchiudono" lo stesso ambiente, permettendo loro di comunicare privatamente modificando l'ambiente che condividono.
Nota: a volte vengono definite chiusure anche le strutture dati che combinano un ambiente lessicale con entità che non sono funzioni, ma comunque il termine solitamente si riferisce specificamente alle funzioni.
Linguaggi di programmazione con le chiusure
[modifica | modifica wikitesto]Scheme è stato il primo linguaggio di programmazione ad avere delle chiusure pienamente generali e con scoping lessicale. Virtualmente, tutti i linguaggi di programmazione funzionali e tutti i linguaggi di programmazione orientati agli oggetti derivati dallo Smalltalk supportano qualche forma di chiusura. Alcuni tra i maggiori linguaggi di programmazione a supportare le chiusure sono:
- ActionScript
- AppleScript
- Boo
- C# dalla versione 2.0
- C++11
- Eiffel
- Go
- Groovy
- Haskell
- Java dalla version 1.8
- JavaScript
- Lisp (inclusi Scheme e Common LISP)
- Lua
- MATLAB
- Tutte le varianti di ML (inclusi OCaml e Standard ML)
- Nemerle
- Oz
- Perl
- PHP dalla versione 5.3
- Python
- Ruby
- Rust
- Scala
- Sleep
- Smalltalk
- Swift
- VB.NET
Simulare le chiusure
[modifica | modifica wikitesto]In C è possibile usare la parola chiave "static" prima della dichiarazione di una variabile locale. Una variabile static conserva il suo valore attraverso le varie chiamate alla funzione. Questo vale anche per il C++.
int Function(int arg)
{
static int numDiChiamate = 0;
static struct Stato infoSulloStato;
return ++numDiChiamate;
}
Alcuni linguaggi orientati agli oggetti permettono al programmatore di usare oggetti che simulano alcune caratteristiche delle chiusure. Per esempio:
- Java permette al programmatore di definire "classi anonime" all'interno di un metodo; una classe anonima può fare riferimento ai nomi dichiarati nelle classi nelle quali è racchiusa e ai nomi di oggetti di tipo
final
dichiarati nel metodo nel quale è racchiusa.
public interface Function<Da,A>
{
A apply(Da unParametro);
}
// altro...
public final Function<Integer,Integer> creaSommatore(final int x)
{
return new Function<Integer,Integer>()
{
public Integer apply(Integer unParametro)
{
return x + unParametro;
}
};
}
// altro...
(Si noti che questo esempio usa degli aspetti del linguaggio disponibili solo a partire dalla versione 1.5/5.0 di Java, come i generics, l'autoboxing e l'autounboxing)
- In C++ e D, i programmatori possono definire classi che fanno l'overload dell'operatore
()
(operatore di applicazione della funzione). Questo genere di classi sono chiamate oggetti funzione o, occasionalmente, funtori (anche se quest'ultimo termine è ambiguo, dato che in altri linguaggi ha un significato molto diverso). Questi oggetti funzione si comportano in modo simile alle funzioni nei linguaggi di programmazione funzionali, ma sono diversi dalle chiusure nel fatto che le variabili del loro ambiente non vengono "catturate". Per simulare una vera chiusura, si può immaginare di mettere tutte le variabili globali in una singola struttura (struct
) e di passarne una copia ad un oggetto funzione. - Il C# permette ad un delegato (
delegate
) di memorizzare un riferimento ad un metodo di un'istanza di una classe (cioè un oggetto); quando verrà chiamato questo delegato, il metodo verrà invocato per quella particolare istanza. Per simulare una vera chiusura, si può creare un'istanza di una classe (cioè un oggetto) con gli attributi necessari. Un altro modo per simulare le chiusure è stato introdotto dalla versione 2.0 del linguaggio, con l'utilizzo dei metodi anonimi. - Nel C++11 è possibile creare una chiusura tramite l'utilizzo di lambda expressions oppure di std::function.
Note
[modifica | modifica wikitesto]Bibliografia
[modifica | modifica wikitesto]- (EN) Will Clinger, Foundations of Actor Semantics (PDF), in AI Technical Reports, n. 633, MIT Artificial Intelligence Laboratory, 1º maggio 1981.
Voci correlate
[modifica | modifica wikitesto]Collegamenti esterni
[modifica | modifica wikitesto]- (EN) Denis Howe, closure, in Free On-line Dictionary of Computing. Disponibile con licenza GFDL