Closure (Funktion)
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.[1]
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.[2]
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,[3] C++ (ab C++11), C#, Go, Groovy, Java,[4] JavaScript,[5] Lua, Object Pascal (Delphi),[6][7] 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.[8]
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 n
geprü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")))
(defun get-name (record)
(first record))
(defun name-is (name)
(lambda (record)
(equal (get-name record) name)))
(defun filter (predicate list)
(remove-if-not predicate list))
Diese Definitionen machen nun folgende elegante Abfrage möglich:
(print (filter (name-is "Gabi") *dbase*))
Sie ist folgendermaßen zu verstehen: Der Funktionsaufruf (name-is "Gabi")
liefert eine Closure. Sie ist hier eine Verbindung aus dem Vergleichscode (equal (get-name record) name)
aus der Funktion name-is
und der Bindung der Zeichenkette "Gabi"
an die Variable name
. Damit handelt es sich semantisch um die Abfrage (equal (get-name record) "Gabi")
. Dieser Vergleich wird als Closure an die Funktion filter
übergeben, die diesen Vergleich anwendet. Ausführen dieser Filterung führt dann zu dem Ergebnis:
(("Gabi" "2.3.1981") ("Gabi" "5.6.1983"))
Perl
Der Kontext eines beliebigen Code-Fragments wird unter anderem durch die zur Verfügung stehenden Symbole bestimmt:
# pragma
use strict;
sub function {
# Argumente in benannte Variablen kopieren
my ($var1, $var2) = @_;
# block code
}
Im oben gezeigten Beispiel sind die Variablen $var1
und $var2
an jeder Stelle der Funktion gültig und sichtbar. Beim Verlassen der Funktion werden sie zusammen mit dem verlassenen Block aufgeräumt („gehen“ out of scope) und sind anschließend unbekannt. Jeder weitere Zugriff wäre ein Fehler.
Closures bieten nun die Möglichkeit, den Gültigkeitsbereich solcher Variablen über dessen offizielles Ende hinaus auszudehnen. Dazu wird im Scope einfach eine Funktion definiert, die die betreffenden Variablen verwendet:
# pragma
use strict;
sub function {
my ($var1, $var2) = @_;
return sub { print "Vars: $var1, $var2.\n" };
}
my $f = function("Hallo", 8);
my $g = function("bar", "Y");
# Aufruf von $f
$f->();
# Aufruf von $g
$g->();
Das Laufzeitsystem stellt jetzt beim Verlassen der Funktion function
fest, dass noch Referenzen auf die Blockvariablen $var1
und $var2
bestehen – der Rückgabewert ist eine anonyme Subroutine, die ihrerseits Verweise auf die Blockvariablen enthält. $var1
und $var2
bleiben deshalb mit ihren aktuellen Werten erhalten. Weil die Funktion auf diese Weise die Variablen konserviert, wird sie zur Closure.
Mit anderen Worten kann man auch nach dem Verlassen des eigentlichen Gültigkeitsbereichs der Variablen jederzeit den Aufruf $f->()
und den Aufruf $g->()
ausführen und wird im Ergebnis immer wieder die bei der Definition der Funktionen gültigen Werte der Variablen angezeigt bekommen.
Dies ergibt die Ausgabe:
Vars: Hallo, 8. Vars: bar, Y.
Ändern kann man diese Werte nicht mehr, da die Variablen außerhalb der Closure nicht mehr verfügbar sind. Das liegt aber vor allem an der Funktionsdefinition: Natürlich hätte die Closure die Werte nicht nur ausgeben, sondern auch bearbeiten oder auch aufrufendem Code wieder per Referenz zur Verfügung stellen können. In der folgenden Variante werden beispielsweise Funktionen zum Inkrementieren und Dekrementieren eingeführt:
# pragma
use strict;
# function
sub function {
my ($var1, $var2) = @_;
return (
sub {print "Vars: $var1, $var2.\n"},
sub {$var1++; $var2++;},
sub {$var1--; $var2--;}
);
}
# call the function
my ($printer, $incrementor, $decrementor) = function(3,5);
# use closures
$printer->();
$incrementor->();
$printer->();
$incrementor->();
$incrementor->();
$printer->();
Dies ergibt die Ausgabe:
Vars: 3, 5. Vars: 4, 6. Vars: 6, 8.
Closures lassen sich also beispielsweise dazu verwenden, um den Zugriff auf sensible Daten zu kapseln.
Python
Folgend ein einfaches Beispiel für einen Zähler in Python, der ohne einen (benannten) Container auskommt, der den aktuellen Zählerstand speichert.
def closure():
container = [0]
def inc():
container[0] += 1
def get():
return container[0]
return inc, get
Im Beispiel werden innerhalb der closure
-Funktion zwei Funktionsobjekte erstellt, die beide die Liste container
aus ihrem jeweils übergeordneten Scope referenzieren. Ist die closure
-Funktion also abgearbeitet (nach einem Aufruf) und werden die beiden zurückgegebenen Funktionsobjekte weiter referenziert, dann existiert die container
-Liste weiter, obwohl der Closure-Scope bereits verlassen wurde. Auf diese Weise wird also die Liste in einem anonymen Scope konserviert. Man kann nicht direkt auf die Liste container
zugreifen. Werden die beiden Funktionsobjekte inc
und get
nicht mehr referenziert, verschwindet auch der Container.
Die Closure im vorigen Beispiel wird dann auf die folgende Weise verwendet:
>>> i, g = closure() >>> g() 0 >>> i() >>> i() >>> g() 2
OCaml
OCaml erlaubt das in folgender Weise:
let counter, inc, reset =
let n = ref 0 in
(function () -> !n), (* counter *)
(function () -> n:= !n + 1), (* incrementor *)
(function () -> n:=0 ) (* reset *)
jetzt ist der Zähler wie folgt anwendbar:
# counter();; (* ergibt 0 *)
# inc();;
# counter();; (* ergibt 1 *)
# inc();inc();inc();;
# counter();; (* ergibt 4 *)
# reset();;
# counter();; (* ergibt 0 *)
# n;; (* n ist gekapselt *)
Unbound value n
Statt einer Ganzzahl („Integer“) können natürlich auf diese Weise beliebige Objekte oder Variablen beliebiger Typen gekapselt werden.
JavaScript
In der Funktion f1 wird eine weitere Funktion f2 als Closure definiert;
let f1 = function() { // eine äußere Funktion f1 definieren ...
let wert = 22; // ... und darin einen Namensraum erstellen.
let f2 = function() { // eine innere Funktion definieren, ...
return wert; // ... die den Namensraum nach außen reicht.
}
return f2; // f2 durch f1 zurückgeben, womit f2 zum closure wird.
}
let a = f1(); // a ist die von f1() zurückgegebene closure-Funktion, ...
console.log(f1()); // ... also: function() {return wert;}
console.log(typeof wert); // ist undefined
console.log(a()); // ergibt 22
console.log(f1()()); // ergibt 22, f2() ist hier aber nicht abrufbar
Obiges Beispiel etwas anders formuliert, die innere Funktion wird jetzt direkt aufgerufen:
let f3 = function() {
let wert = 23;
// die Funktion f3 gibt gleich die closure-Funktion zurück!
return function() {
return wert;
};
}
let b = f3(); // b ist wieder die von f3() zurückgegebene Funktion ...
console.log(b()); // ... und liefert jetzt als Ergebnis 23
console.log(b); // b bleibt aber weiterhin ein Funktionsaufruf!
console.log(f3()()); // liefert ebenfalls 23
Die eingebettete Funktion dient jeweils als Lieferant des in der übergeordneten Funktion definierten Wertes.
Die übergeordnete Funktion kann auch als anonyme Funktion definiert werden:
let wert = 24;
let c = (function() { // die äußere als anonyme Funktion und ...
return wert; // ... darin die innere Funktion definieren.
}()); // die Funktion jetzt noch mit (); aufrufen.
console.log(c); // ergibt 24
Die Closure kann auch mit einer Konstruktorfunktion erzeugt werden:
let d = (new Function("return wert;"))(); // mit einem Konstruktor definieren und aufrufen
Lua
Lua hat eine eingebaute und im Sinne der Programmierung auch intuitiv nutzbare Unterstützung für Closures, deren Implementierung ähnlich derjenigen in Python ist:
function adder(x) -- Funktionserzeuger
return function(y) -- anonyme, zu adder private Funktion
return x+y -- x stammt hier aus dem äußeren Kontext
end
end
Eine Beispielnutzung sähe so aus:
add2 = adder(2) -- hier wird die Closure erzeugt
print(add2(10)) --> Ausgabe 12
print(add2(-2)) --> Ausgabe 0
Eine Closure-Implementierung in Lua ist in[9] beschrieben.
Erlang
Erlang als funktionale Sprache besitzt ebenfalls Closures, die allerdings Funs (Singular Fun, von function) genannt werden.
do_something(Fun) -> Fun(4).
main() ->
Var = 37,
F = fun(N) -> Var + N end.
Result = do_something(F).
% Result =:= 41 =:= 37 + 4
C#
C# unterstützt Closures in Form von Delegates.[10]
private static Action CreateClosure()
{
// Deklaration einer Variablen im lokalen Kontext
var x = 0;
// Erstellung eines Closure-Delegate mit Hilfe eines Lambda-Ausdrucks
Action closure = () => Console.WriteLine(x);
// Änderung am lokalen Kontext
x = 1;
// Rückgabe der Closure in den übergeordneten Kontext
return closure;
}
static void Main()
{
var closure = CreateClosure();
// Im globalen Kontext
// Variable x wird nur noch innerhalb der Closure referenziert
// Führe Closure aus; Schreibt "1" auf die Konsole
closure();
}
C++
C++ unterstützt Closures mittels Lambda-Ausdrücken[11] (ab C++11), die sich in Funktionsobjekte, sogenannte Funktoren, des Typs std::function kapseln lassen.
#include <string>
#include <iostream>
#include <functional>
std::function<void(void)> create_closure() {
std::string kuchen("Apfelkuchen");
// Lokale Variablen werden hier als Kopie in das Funktionsobjekt übertragen
return [=]() { std::cout << "Ich esse " << kuchen << std::endl; };
}
int main() {
std::function<void(void)> closure = create_closure();
closure();
return 0;
}
Mit Hilfe des Schlüsselworts mutable kann aus einer Lambda-Funktion eine echte Closure erstellt werden, die nicht nur ihre eigenen Variablen besitzt, sondern diese auch verändern kann (die Variable „anzahl_kuchen“ im äußeren Block wird dabei jedoch nicht verändert, sondern nur eine Kopie davon):
#include <iostream>
#include <functional>
std::function<void(void)> mutterfunktion() {
int anzahl_kuchen = 0;
// Die übernommene Kopie der Variable kann hier zusätzlich ihren Wert verändern.
return [=]() mutable { std::cout << "Ich esse " << ++anzahl_kuchen << " Kuchen.\n"; };
}
int main() {
std::function<void(void)> essen = mutterfunktion();
essen();
essen();
essen();
return 0;
}
Ausgabe dieses Programms:
Ich esse 1 Kuchen. Ich esse 2 Kuchen. Ich esse 3 Kuchen.
Java
In Java sind ab der Version 8 ebenfalls Closures möglich, wobei dabei einige spezifische Annahmen der Sprache über Lambda-Ausdrücke zu beachten sind. Der folgende Code würde zum Beispiel nicht kompilieren.
private static Function<String, Supplier<String>> generator = kuchenname -> {
int zähler = 0;
return () -> "Ich esse " + zähler++ + " " + kuchenname; // Fehler: zähler kann nicht geändert werden
};
public static void main(String[] args) {
Supplier<String> käsekuchen = generator.apply("Käsekuchen");
System.out.println(käsekuchen.get());
System.out.println(käsekuchen.get());
System.out.println(käsekuchen.get());
}
In Java kann der Code innerhalb eines Lambda-Ausdrucks lesend auf die Variablen der umschließenden Methode zugreifen, kann sie jedoch nicht verändern. Im obigen Beispiel versucht der Code des zurückgegebenen Suppliers, durch zähler++
den Wert einer Variable zu ändern, was einen Compilerfehler auslöst. Um diese Einschränkung zu umgehen, müssen Daten, die verändert werden, in Objekten gekapselt werden, zum Beispiel mit AtomicInteger:
private static Function<String, Supplier<String>> generator = kuchenname -> {
AtomicInteger zähler = new AtomicInteger(0);
return () -> "Ich esse " + zähler.getAndIncrement() + " " + kuchenname;
};
public static void main(String[] args) {
Supplier<String> käsekuchen = generator.apply("Käsekuchen");
System.out.println(käsekuchen.get());
System.out.println(käsekuchen.get());
System.out.println(käsekuchen.get());
}
Der so korrigierte Code kompiliert, da die Referenz auf das Zählerobjekt im Lambda-Ausdruck unverändert bleibt. Die Ausgabe ist dann:
Ich esse 0 Käsekuchen Ich esse 1 Käsekuchen Ich esse 2 Käsekuchen
PHP
PHP unterstützt Closures ab Version 5.3.0 in Form anonymer Funktionen.[12]
Technisch löst PHP die Umsetzung dieser Funktionalität durch eine eigene „Closure“-Klasse.[13]
$mutterfunktion = function() {
$anzahl_kuchen = 0;
$kindfunktion = function() use (&$anzahl_kuchen) {
$anzahl_kuchen = $anzahl_kuchen + 1;
print "Ich esse {$anzahl_kuchen} Kuchen\n";
};
return $kindfunktion;
};
$essen = $mutterfunktion();
$essen();
$essen();
$essen();
Die Ausgabe der Aufrufe lautet wie folgt:
Ich esse 1 Kuchen Ich esse 2 Kuchen Ich esse 3 Kuchen
Ab PHP 7.0 werden Closures zusätzlich auch in Form anonymer Klassen unterstützt.[14]
$essen = new class() {
private $anzahl_kuchen = 0;
public function __invoke() {
$this->anzahl_kuchen = $this->anzahl_kuchen + 1;
print "Ich esse {$this->anzahl_kuchen} Kuchen\n";
}
};
$essen();
$essen();
$essen();
Beide Implementierungen liefern identische Ausgaben.
Rust
Rust unterstützte Closures bereits ab Version 0.1, die Rückgabe von Closures aus Funktionen musste bis zu Rust 1.26 (veröffentlicht am 10. Mai 2018) über einen Zeiger auf den Heap-Speicher (via Box
) geschehen.
fn mutterfunktion() -> Box<dyn FnMut() -> ()> {
let mut anzahl_kuchen = 0;
let kindfunktion = move || {
anzahl_kuchen += 1;
println!("Ich esse {} Kuchen", anzahl_kuchen);
};
// [Ex.1] Fehler wenn anzahl_kuchen nicht Copy implementieren würde (s.u.)
// println!("Jetzt ist die Anzahl der Kuchen: {}", anzahl_kuchen);
return Box::new(kindfunktion);
}
fn main() {
let mut essen = mutterfunktion();
essen();
essen();
essen();
}
Ausgabe:
Ich esse 1 Kuchen Ich esse 2 Kuchen Ich esse 3 Kuchen
In Rust 1.26 wurde die impl Trait
Syntax stabilisiert, welche den gleichen Code ohne Indirektion via Heap-Speicher (Box::new()
) ermöglicht:
fn mutterfunktion() -> impl FnMut() -> () {
let mut anzahl_kuchen = 0;
move || {
anzahl_kuchen += 1;
println!("Ich esse {} Kuchen", anzahl_kuchen);
}
}
fn main() {
let mut essen = mutterfunktion();
essen();
essen();
essen();
}
Hierbei „implementiert“ der Rückgabewert von mutterfunktion()
den Fn
trait, wobei die Ermittlung des exakten Typs des Rückgabewerts erst bei der Nutzung der Funktion erfolgt.
Rust differenziert hierbei zwischen Funktionszeigern und Closures, sowie verschiedenen Closure-Typen: Fn
, FnMut
und FnOnce
. Eine Fn
-Closure kann den Kontext, in dem sie aufgerufen wird, nicht modifizieren. Eine FnMut
-Closure kann die Variable im Kontext nur modifizieren, wenn diese als mut
gekennzeichnet wurde. Eine FnOnce
-Closure konsumiert die im Kontext erstellte Variable. Hierbei könnte essen()
nur exakt einmal aufgerufen werden – am Ende der ersten Closure läuft der Destruktor von anzahl_kuchen
und die Variable ist damit nicht mehr verfügbar.
Wenn [Ex.1]
auskommentiert wird, ergibt die Ausgabe:
Jetzt ist die Anzahl der Kuchen: 0 Ich esse 1 Kuchen Ich esse 2 Kuchen Ich esse 3 Kuchen
Das move
Schlüsselwort wird gebraucht um den Besitz der Variable anzahl_kuchen
anzuzeigen. Da unsere Variable anzahl_kuchen
kopierbar ist (Variablen des Typs u32
implementieren den Copy
-Trait), können wir die Variable innerhalb der Mutterfunktion noch verwenden, nachdem der eigentliche Wert der Closure übergeben wurde. Hierbei wird anzahl_kuchen
kopiert, d. h. obwohl wir im Code die Anzahl bereits auf 1 gesetzt haben, gibt die Ausgabe noch 0 aus, da es eine komplette Kopie der Variable ist. Ist der Typ von anzahl_kuchen
nicht kopierbar, gibt der Compiler einen Fehler aus.
Scala
Scala ist eine funktionale Programmiersprache auf Basis der Java Virtual Machine.
object ClosureTest {
def main(args: Array[String]): Unit = {
def kindfunktion = mutterfunktion
println("Calling Kinder")
kindfunktion;
kindfunktion;
kindfunktion;
}
var anzahl_kuchen = 0;
def mutterfunktion : Unit = {
var kuchentype = "Apfelkuchen"
def kindfunktion : Unit = {
anzahl_kuchen = anzahl_kuchen + 1
return System.out.println("Ich esse " + anzahl_kuchen.toString() + " " + kuchentype)
}
kindfunktion
}
}
Hierbei ist die Variable anzahl_kuchen über Aufrufe hinweg nur veränderlich, wenn sie in einem globalen Kontext (durchaus auch in einer anderen Klasse oder einem anderen Objekt) definiert wurde. Die Ausgabe des Programms lautet:
Calling Kinder Ich esse 1 Apfelkuchen Ich esse 2 Apfelkuchen Ich esse 3 Apfelkuchen
Literatur
- Ralf H. Güting, Martin Erwig, Übersetzerbau. Springer, 1999, ISBN 3-540-65389-9
- Damian Conway, Object Oriented Perl
- Oliver Lau, Andreas Linke, Torsten T. Will: Variablen to go – Closures in aktuellen Programmiersprachen. In: c’t, 17/2013, S. 168ff.
Weblinks
Einzelnachweise
- ↑ Ted Neward: Closure-based State: C#. 2. April 2016, archiviert vom (nicht mehr online verfügbar) am 23. Januar 2022; abgerufen am 14. April 2022 (englisch).
- ↑ John McCarthy u. a.: Lisp 1.5 Programmers Manual. (PDF; 4,5 MB) softwarepreservation.org; abgerufen am 12. März 2014.
- ↑ John Barnes: Rationale for Ada 2005
- ↑ Closures in Java
- ↑ Closures in JavaScript (englisch)
- ↑ Craig Stuntz: Understanding Anonymous Methods. In: community.embarcadero.com. 4. August 2008, abgerufen am 14. April 2022 (englisch).
- ↑ Barry Kelly: Tiburon: fun with generics and anonymous methods.
- ↑ N1370: Apple’s Extensions to C (PDF; 69 kB)
- ↑ The implementation of Lua 5.0
- ↑ Dustin Campbell: What’s In A Closure. 9. Februar 2007, archiviert vom (nicht mehr online verfügbar) am 15. August 2014; abgerufen am 12. April 2014 (englisch).
- ↑ Lambda functions. Abgerufen am 17. Oktober 2015 (englisch).
- ↑ Anonymous functions. Abgerufen am 19. Mai 2015.
- ↑ The Closure class. Abgerufen am 19. Mai 2015.
- ↑ Joe Watkin, Phil Sturgeon: Anonymous Classes. 22. September 2013, abgerufen am 19. Mai 2015 (englisch).