Eine Closure ist ein Konzept aus der funktionalen Programmierung. Wenn eine Funktion, die freie Variablen verwendet, den Scope verlässt, in dem diese vereinbart sind (meistens, weil sie selbst von einer Funktion zurückgegeben wird), und wird dieser Scope abgeräumt, so wären diese freien Variablen ab diesem Zeitpunkt undefiniert. Um dem zu begegnen, setzt der Compiler bei der Rückgabe eine neue Struktur zusammen. Sie besteht aus dieser Funktion und sämtlichen von ihr verwendeten freien Variablen. Diese Struktur heißt Closure.
Der Name deutet an, dass die betroffenen Variablenbindungen ab diesem Zeitpunkt nicht mehr von außerhalb der Closure erreicht werden können (Es gibt Ausnahmen, falls mehrere Closures gleichzeitig erzeugt werden, s. u.).
Die Closure wird von Laufzeitsystem und Compiler von reinen Funktionen unterschieden. Sie muss auch logisch von diesen unterschieden werden, da sie durch die in ihr enthaltenen (auch: „ge-closure-ten“) Variablenbindungen innere Zustände hat und darum in aufeinanderfolgenden Aufrufen unterschiedliche Werte für gleiche Argumente liefern kann.
Daher kann man eine Closure auch als Objekt ansehen, das in der Regel nur eine Methode hat. Wird die Closure zusammen mit weiteren Closures in demselben Kontext erzeugt, so handelt es sich um ein Objekt mit mehreren Methoden. Die in der Closure eingeschlossenen Variablen aus dem erzeugenden Bereich können von der Closure als Attribute verwendet werden.
Herkunft Closures sind ein Konzept, das aus den funktionalen Programmiersprachen stammt, zum ersten Mal in Lisp auftrat und in seinem Dialekt Scheme erstmals vollständig unterstützt wurde. Daraufhin wurde es auch in den meisten späteren funktionalen Programmiersprachen (etwa Haskell, Ocaml) unterstützt.
Vorteile und Eigenschaften Mit Closures können nicht sichtbare, aber kontrolliert veränderbare Bereiche erstellt werden, beispielsweise kann damit Datenkapselung realisiert oder Currying umgesetzt werden.
Die Erzeugung einer Closure ist mit deutlich weniger Arbeit verbunden als die Erstellung einer Klasse mit nur einer Methode. Objektorientierter Sichtweise folgend eignen sich Closures so zur schnellen Erzeugung einer objektähnlichen Struktur ohne eine Klasse. Oftmals wird als innere Methode eine anonyme Funktion verwendet.
In einer rein funktionalen Programmiersprache kann eine Closure immer dann verwendet werden, wenn sie entweder selbst als Funktion aufgerufen oder als Parameter in einen Funktionsaufruf eingehen soll. Im letzteren Fall kann sie als zur Laufzeit erzeugte Call-Back-Funktion agieren und ermöglicht so einem Anwendungsprogramm in erheblichen Maß während seiner Laufzeit den eigenen Kontrollfluss zu manipulieren. Dies wird allerdings meistens erst durch ein System von Closures praktisch sinnvoll ermöglicht. Auf dieser Tatsache beruht das didaktische Problem, unerfahrenen Programmierern die Anwendung von Closures nahezubringen.
Beispiel in Pseudocode Im folgenden Beispiel wird zunächst eine Funktion mutterfunktion definiert. Diese Funktion setzt eine lokale Variable namens kuchentyp und definiert eine lokale Funktion namens kindfunktion.
funktion mutterfunktion { setze kuchentyp = 'Apfelkuchen'
funktion kindfunktion { gib_aus 'Ich esse #{kuchentyp}'
gib_zurück kindfunktion
Bei einem Aufruf gibt mutterfunktion die lokale Funktion kindfunktion (nicht deren Ergebnis!) zurück. (Dies ist in nicht funktionalen Programmiersprachen wie C und Verwandten technisch auch als Funktionszeiger bekannt. Ein typisierter Funktionszeiger heißt Delegate.)
setze meinkuchen = rufe_auf mutterfunktion
Die globale Variable meinkuchen bekommt also die Funktion kindfunktion zugewiesen.
rufe_auf meinkuchen
Beim anschließenden Aufruf von meinkuchen wird folglich kindfunktion ausgeführt. Obwohl keine globale Variable kuchentyp existiert, gibt kindfunktion die Zeichenkette 'Ich esse Apfelkuchen' aus, weil sie auf ihren Erstellungskontext zugreifen kann, in dem die Variable kuchentyp mit 'Apfelkuchen' definiert ist. Entscheidend ist dabei: Obwohl mutterfunktion schon einen Wert zurückgegeben hat – der Kontext also eigentlich nicht mehr existiert – kann kindfunktion darauf zugreifen – kindfunktion ist also eine Closure-Funktion.
[Ausgabe:] Ich esse Apfelkuchen
Mit einer Änderung im Code wird nun der Wert der Variablen anzahl_kuchen in der mutterfunktion mit jedem Zugriff auf die Closure-Funktion um eins erhöht, womit sich ein Zähler realisieren lässt. Der Wert in anzahl_kuchen ist vor Manipulation geschützt und kann nur durch essen erhöht werden.
funktion mutterfunktion { setze anzahl_kuchen = 0
funktion kindfunktion { setze anzahl_kuchen = anzahl_kuchen + 1 gib_aus 'Ich esse #{anzahl_kuchen} Kuchen'
gib_zurück kindfunktion
Mit mehrfachen Aufrufen der Mutterfunktion von anderen Programmteilen aus kann nur indirekt auf den eigentlich nicht mehr sichtbaren Wert der lokalen Variable <anzahl_kuchen> zugegriffen werden, und (nur) innerhalb der kindfunktion können (gekapselte) Berechnungen mit sonst nicht veränderbaren Werten vorgenommen werden – das zeigt die erwähnten Hauptvorteile von Closures:
setze essen = rufe_auf mutterfunktion rufe_auf essen rufe_auf essen rufe_auf essen
Ich esse 1 Kuchen Ich esse 2 Kuchen Ich esse 3 Kuchen
Der direkte Zugriff auf die Variable anzahl_kuchen ist so geschützt, ihr Wert kann (wie im Beispiel) oder könnte auch nicht direkt nach außen gereicht werden. Keinesfalls ist der Wert aber von außen veränderbar, damit bieten Closures mehr Zugriffsschutz als etwa als „private“ deklarierte Felder einer Klasse etwa in Java oder C#, der etwa mit Reflection einfach zu umgehen ist.
Wie man dies interpretiert, hängt stark von der eigenen Sichtweise auf Programmiersprachen ab. Die Mutterfunktion übernimmt aus objektorientierter Sichtweise die Rolle einer Klasse, genauer eines Objekts (der Instanz einer Klasse) und kapselt aus objektorientierter Sicht so Kindvariablen mit Kindfunktion(en) zu einer Einheit.
Anders gesehen wird so eine Art aufrufübergreifendes „Gedächtnis“ in den Funktionen implementiert, ähnlich einer statischen Variablen, nur leistungsfähiger. Noch ein wenig anders betrachtet kann man dies auch als Veränderung des Kontrollfluss ansehen wie obiges Beispiel sehr gut zeigt. Aufzählungen können etwa als Funktionsaufruf implementiert werden, da bei jedem Aufruf (aufgrund des „Gedächtnisses“) ein anderes Ergebnis geliefert werden kann. C# nutzt dies als Spezialfall etwa bei der Implementierung von „yield return“. Dabei wird pro Aufruf Schritt für Schritt das nächste Element eines aufzählbaren Typs wie einer Liste, sozusagen „faul“ (lazy), d. h. ressourcensparend nur bei Bedarf zurückzugeben.
Konzeptionelle Voraussetzungen für Closures in Programmiersprachen
Closures stellen wie erwähnt ein Muster funktionaler Programmierung dar, sie sind für Programmierer nicht rein funktionaler Programmiersprachen oftmals schwer zu verstehen, auch wenn sie in zunehmend mehr Programmiersprachen umsetzbar sind.
Folgende konzeptionelle „Bausteine“ sind nötig, um eine Closure in einer Programmiersprache umsetzbar zu machen.
1. Funktionen müssen als Rückgabeobjekte einer anderen Funktion erlaubt sein, mindestens über zu Hilfe genommene Elemente wie Funktionszeiger, Delegates oder Lambda-Ausdrücke. Man spricht hier auch von First-Class-Funktionen. (Das Gegenteil ist insbesondere der Fall, wenn Funktionen lediglich als eine Art benannter Befehl betrachtet und verwendet werden können).
2. In obigem Beispiel muss die innere Funktion auf die Variablen der äußeren Funktion (Aufrufumgebung) zugreifen können. Diese Variablen werden im Unterschied zu lokalen Variablen aus Sicht der inneren Funktion auch als „freie Variablen“ (englisch „free variables“) bezeichnet.
3. Der Compiler muss in der Lage sein, zu erkennen, dass der Wert (Zustand) der Variablen außerhalb deren eigentlichen Gültigkeitsbereich (scope) benötigt wird, und dies bei der Kompilierung aktiv berücksichtigen. Technisch werden diese Variablen dann meist nicht mehr auf dem Stack abgelegt, sondern dies wird anders gelöst, z. B. indem tatsächlich im Hintergrund eine (anonyme) Klasse samt Instanz erzeugt wird, die die benötigten (Member)variablen und die innere Funktion (als Memberfunktion) enthält.
Jetzt erst sind alle Bausteine beisammen, um eine verkürzte, aber technischere Definition des Begriffes Closure aufzustellen, genau genommen von lexikalischen Closures im engeren Sinne:
Closures sind also eine Programmiertechnik bzw. Strukturen, um lexikalische Skopierung (englisch scope) mit freien Variablen in Sprachen mit First-Class-Funktionen umzusetzen.
Dynamische und lexikalische Closures Die erste Implementierung von Closures ergab sich aus der Art der Implementierung von Ausführungsumgebungen in Lisp. In den ersten Lisp-Implementierungen gab es keine lexikalische Skopierung. Die Ausführungsumgebung einer Anweisung bestand aus einer sogenannten A-Liste mit Variablenbindungen, die über eine einzelne Referenz erreichbar war. Eine Closure über einer Funktion bestand dann aus einem Paar, bestehend aus der Funktionsdefinition und der Referenz auf die zur Definitionszeit der Closure gültigen A-Liste. Dieses durch die Lisp-Funktion FUNCTION erzeugte Paar ist eine dynamische Closure mit der historischen Bezeichnung FUNARG (FUNctional ARGument). Gelangte das FUNARG später zur Ausführung, so geschah dies im Kontext der mitgebrachten A-Liste anstatt im Kontext der aktuell gültigen A-Liste.John McCarthy u. a.: Lisp 1.5 Programmers Manual. (PDF; 4,5 MB) softwarepreservation.org; abgerufen am 12. März 2014.
Die heute in Lisp wie in allen anderen Sprachen verwendete lexikalische Skopierung führt zur lexikalischen Closure, die auch in kompilierten Sprachen funktionsfähig ist. Sie entsteht erst durch aktives Eingreifen des Compilers, indem dieser die Bezüge der Funktion auf die innerhalb ihrer selbst freien und außerhalb von ihr gebundenen Variablen identifiziert und Code erzeugt, der diese Bindungen mit der Funktion zusammen bei ihrer Rückgabe aus ihrem Definitionskontext zu einer Closure zusammensetzt. Dies geschieht, bevor diese Funktion – nun als Closure – dem Aufrufer zur Verfügung gestellt wird. Da diese Variablenbindung nun nicht mehr lexikalisch gebunden ist, kann sie nicht auf dem Stack verbleiben, sondern wird vom Laufzeitsystem auf den Heap gelegt. Bei gleichzeitiger Bildung mehrerer Closures über derselben Variablenbindung sorgt das Laufzeitsystem dafür, dass in beide Closures dieselbe Heap-basierte Kopie dieser Variablenbindung eingesetzt wird.
Implementierungen Es existieren auch nicht-funktionale Programmiersprachen, die diese Funktion unterstützen. Dazu gehören Ada,John Barnes: Rationale for Ada 2005 C++ (ab C++11), C#, Go, Groovy, Java,Closures in Java JavaScript,Closures in JavaScript (englisch) Lua, Object Pascal (Delphi),Barry Kelly: Tiburon: fun with generics and anonymous methods. PHP, Perl, Python, Ruby, Smalltalk, Swift und Visual Basic .NET. Apple hat den gcc und Clang um Closures – genannt Block-Literals – für C erweitert und dies zur Standardisierung vorgeschlagen.N1370: Apple’s Extensions to C (PDF; 69 kB)
Beispiele von Implementierungen
Common Lisp Dieses Beispiel verwendet eine Closure, um eine elegante Datenbankabfrage zu ermöglichen. Die Closure wird von der Funktion name-is geliefert. Durch die special function lambda wird eine namenlose Funktion erzeugt, innerhalb derer der Wert des Feldes name auf die Gleichheit mit einer Zeichenkette ngeprüft wird. Der Aufruf (name-is "Elke") liefert also eine Closure als Verbindung aus der anonymen Funktion und der Variablenbindung von n an die Zeichenkette „Elke“. Diese kann einen Datensatz auf den Namensgleichheit mit „Elke“ überprüfen. Die Closure kann direkt an die Funktion filter übergeben werden, die diese dann anwendet und das Ergebnis zurückgibt.
(defparameter *dbase* '(("Elke" "1.1.1980") ("Gabi" "2.3.1981") ("Heidi" "4.5.1982") ("Gabi" "5.6.1983") ("Uschi" "7.8.1984")))