Closure als Mittler zwischen Funktionen und Objekten

Closure öffne dich!
Kommentare

Eine Closure ist ein funktionales Objekt, das sich Werte unabhängig seines Geltungsbereichs (Scope) merkt und zurückgibt. Andere meinen auch, sie sei schlicht ein anonymer Codeblock mit Widerverwendung. Wenn Sie jemals eine Funktion gebaut haben, die eine andere Funktion zurückgibt, hatten Sie bereits mit der Closure-Idee Kontakt. Dieser sprachübergreifende Bericht soll Licht in die Welt der Closures bringen und so ganz nebenbei einige Tatsachen zur funktionalen Programmierung liefern (Back to Function).

Ich betrachte eine Closure als Vermittler zwischen Funktionen und Objekten: agil, wie eine Funktion, und zudem als Referenz kompakt, wie ein Objekt. Closure-nahe Implementierungen sind einfach zu realisieren, sei es mit inneren Klassen, Prozedurtypen, Funktionszeigern oder (anonymen) Delegaten, jedoch reicht das in der Regel noch nicht aus. Warum nicht? Weil bei allen der Scope eingeschränkt ist. Wie übersetzt man eigentlich Closure? Abschließer oder Auflöser? Das klingt etwa so fremd wie den Spring Controller als Frühlingsüberwacher zu bezeichnen. Closure kommt von „schließen“ und soll das Abschließen eines Ausdrucks in einer Methode verdeutlichen. Das klingt abstrakt, wird sich gegen Ende des Artikels aber konkretisieren. Der Ursprung des Closure-Konzepts liegt in den bereits erwähnten, funktionalen Programmiersprachen wie Lisp, Haskell oder Ruby (Lisp schon im Jahr 1959). Das Closure-Konzept selbst hat man bereits in den 60er-Jahren kreiert und erstmals in der Sprache Scheme, einem Dialekt der Sprache Lisp, umgesetzt (nota bene noch ohne Objekte). Damit sich Closures auch in prozeduralen Programmiersprachen wie C oder Pascal verwenden lassen, muss es möglich sein, Funktionen als Parameter übergeben zu können. Das ist bei Pascal oder in C mit einem Prozedurtyp als Schnittstelle möglich, genügt aber noch nicht, wie es in Listing 1 zu sehen ist.

Type
  TMath_func = Procedure(var x: float);  //Procedure Type

procedure fct1(var x: float);  //Implement
  begin
    x:= Sin(x);
  end;
var
  fct1x:= @fct1 //Referenz
  fct_table(0.1, 0.7, 0.4, fct1x); //Funktion als Parameter
  fct_table(0.1, 0.7, 0.4, @fct1); //alternativ Direkt ohne Referenz

Mit diesem Konstrukt lässt sich eine Aufrufkette bilden, in der Resultate einer Berechnung einen weiteren Funktionsaufruf auslösen, da man den Funktionszeiger munter weiterreichen kann und der eben auch zurückgerufen wird. Man sieht, die Sprachen C und Pascal sind mehr oder weniger streng typisiert und operieren mit echten Zeigern, im Weiteren betrachten wir dynamische Sprachen mit Referenztypen statt Pointern.

Konzept und Mächtigkeit sind in Closure-fähigen Sprachen in etwa ausgeglichen, man wird ohnehin mit in den Sprachen unterschiedlicher Syntax und Typen konfrontiert, beispielsweise mit Action- und Formular-, Jason-XML-Konfigurations-, JavaScript- und auch HTML- oder JSP-Konstrukten, die sich dann mit Closures eleganter bearbeiten lassen. Es stellt sich manchmal die Frage, ob es angesichts der ohnehin hohen Komplexität anzuraten ist, Groovy, Ruby, Scala oder Dart in einem Projekt zusätzlich einzuführen, nur weil man mit Closures arbeiten will, die in Java offensichtlich noch nicht implementiert sind [1].

So, genug der Vorgeschichte, schauen wir nun in eine Closure hinein, die auf Python setzt (Listing 2). Die innere Funktion nth_power(x) ist eine Closure, da sie Zugriff auf den Parameter n hat, der von der äußeren Funktion generate_power_func(n) gekapselt wird (Enclosing Scope). Als Rückgabe dient die innere Funktion, also die Closure. Das Erstaunliche ist, auch wenn der Programmfluss die Funktion verlässt, ist der Zugriff auf die lokale Variable n immer noch möglich. Hier spricht man von einer externen lokalen Variablen, die Sprache Lua hat mal den Begriff „Upvalue“ geprägt. Die Funktion nth_power(x) schließt also n mit ein. Nun rufen wir die Closure auf und weisen den Funktionszeiger einer Variablen (Referenz) zu:

def generate_power_func(n):
    print "id(n): %X" % id(n)
    def nth_power(x): //Closure
       return x**n
    print "id(nth_power): %X" % id(nth_power)
  return nth_power
>>> raised_to_4 = generate_power_func(4)

Wie erwartet, erzeugt der Aufruf ein Funktionsobjekt und speichert es in raised_to_4. Im idDebugger lässt sich das als Zuweisungsoperator erkennen:

id(raised_to_4) == 0x00C47561 == id(nth_power))

Nun löschen wir die Funktion aus dem globalen Namensraum mit >>> del generate_power_func und rufen die Closure einfach auf (it’s time for Magic):

>>> raised_to_4(2)

Als Resultat erhalten wir 16. Das Erstaunliche ist der angeblich lokale Parameter n=4, der der Closure bekannt ist und somit als vollständige Funktion dient. Das nth_power-Funktionsobjekt kennt also die internen Details der umschließenden Funktion im Scope und ist deshalb eine Closure. Das Fantastische ist nun, dass ein lokales Wiederverwenden von Codeblöcken (Snippets) oder ein Refactoring mit dem Extrahieren einer Methode nun viel eleganter und effizienter möglich ist, und durch den globalen Scope garantiert zu weniger Duplicated-Code führt, frei nach dem Motto „don’t repeat yourself“ (aka dry).

Als weiteres Beispiel einer Anwendung will ich in einer bestehenden Methode eine Ausgabe als Liste einmalig definieren. Ohne Closure benötige ich eine neue Methode im zugehörigen Objekt, als Closure genügt das Einschließen des Blocks mit einer zugehörigen Referenz für den späteren Aufruf (Listing 3). Das Beispiel soll nur zeigen, dass Sprachen ohne Closures viel Substanz vergeben oder redundant sind, indem häufig Codeblöcke dupliziert werden.

mylst:= TStringList.create;
  with TSession.Create(NIL) do try
    SessionName:= 'Mars3'
    getAliasNames(mylst);
    for i:= 1 to mylst.count-1 do  //Define as Closure: var Outlist{}
      write(mylst[i]+' ');
  finally
    Free;
    mylst.Free; 
  end;

Closures sind auch sehr nützlich für Call-Back-Methoden oder die in C# und Delphi bekannten Delegates und Method Pointer. Wobei die anonymen Delegates und Closures zwei unterschiedliche Konzepte darstellen. Denn die Effizienz als Funktion bei den Closures ist eben besser, als ständig ein neues Objekt zu definieren. So ein Delegate benötigt neben der Struktur, dem Typ und dem Konstruktor auch ein Main; die Closure lässt sich direkt einer Referenz zuweisen oder eben als Parameter übergeben. Hier will ich das Design Pattern Visitor erwähnen, dass von der Idee her eine Closure am besten verdeutlicht. Ein einfaches Beispiel, nun in PHP, soll nochmals mit einer konkreten Referenz t3 den gewöhnungsbedürftigen Scope und das zugehörige Konstrukt verdeutlichen (Listing 4).

def times(n):
def _f(x):
  return x * n
return _f
t3 = times(3)
 print t3 #
 print t3(7) # 21

Auch hier wird wieder deutlich, die äußere Funktion times(n) hat als Rückgabewert die innere Funktion _f(), hier als Lambda Expression (eine Lambda Expression ermöglicht das Kürzen einer Funktionsdeklaration). Mit der Referenz t3 wird der Closure der Parameter n=3 bekannt gemacht, den sie beim Aufruf von t3(7) verwerten kann. Festzuhalten ist, dass die Deklaration zur Entwurfszeit nicht mit dem Verhalten zur Laufzeit übereinstimmt, hier werkelt die Compiler Magic indem die lokalen Variablen ja als externe Referenzen auf einem Stack gespeichert sind. Mit anderen Worten: Wenn times(n) ausgeführt wird, liegt eine Referenz des Closure-Objekts _f() auf dem Stack; später lässt sich das Closure-Objekt jederzeit benutzen.

Zitat aus Wikipedia: „The closure object can later be invoked, which results in execution of the body, yielding the value of the expression (if one was present) to the invoker. A closure expression can have parameters, which act as variables whose scope is the body”. Fassen wir nochmals das Grundprinzip in Listing 5 zusammen. Eine äußere Funktion (body) erweitert die innere Closure mit ihren Variablen und Parametern zur Laufzeit. Das Sequenzdiagramm in Abbildung 1 visualisiert die Closure als moderne Call-Back-Methode und zeigt auch das Prinzip der so genannten Inversion of Control (IoC).

>>> def outer(x):
...   def inner(y):
...     return x+y
...   return inner
...
>>> customInner=outer(2)
>>> customInner(3)
result: 5

 

Abb. 1: Im Diagramm ersichtlich ist die Übergabe und der Rückruf

Closures als Threads und Class Helpers

Closures implementieren, zumindest bei Groovy, von Hause aus das Interface Runnable und lassen sich somit auch asynchron ausführen. Im folgenden Beispiel starte ich zwei Closures als parallel laufende Threads:

groovy> Thread.start { ('A'..'Z').each {sleep 100; println it} }
groovy> Thread.start { (1..26).each {sleep 100; println it} }
A
1
B
2

Jeder einzelne Thread schreibt eine Liste von Zeichen (Buchstaben) beziehungsweise Zahlen, wobei er bei jedem Durchlauf eine kurze Pause einlegt, damit jeweils der andere Thread zum Zug kommt – und es für uns verständlich wird, da ansonsten die Reihenfolge unbestimmt ist. Übrigens einige der Konzepte und Muster der Closures lassen sich auch mit der maXbox (es muss nicht immer eine neue JVM-Sprache sein) nachvollziehen, die man unter [2] kostenlos beziehen kann. Dazu steht ein Script bereit: 271_closures_study.txt.

Abschließend noch ein konkretes Funktionsmuster mit einer Klasse, die mit einer Closure als Logger zusammenarbeitet – in Java oder Delphi hätte man eine Inner oder Nested Class genommen. Delphi 2010 kennt auch anonyme Methoden/Funktionen, die einer Closure sehr nahe kommen. Warum heißen diese anonym? Ganz einfach, weil man sie nur via Referenz und nicht über einen Namen aufrufen kann. Wenn man die anonymen Methoden noch mit dem Scope erweitert, hat man Closures, die in Delphi 2010 auch realisiert sind.

Wir bauen abschließend eine Klasse, die irgendwas implementiert, das aber in einer flexiblen Weise protokollieren soll. Die Protokollmethode ist durch eine property definiert, der wir eine Closure zur Ausgabe des Protokolltextes zuweisen (Listing 6). Hier hat die Closure gleich zwei Parameter, die nach der Zuweisung im Kontext der Methode somework arbeiten:

class LoggingWorker {
  Closure log = { step,text ->}
  def somework () {
    log(0, "somework started")
    //...
    log(1, "Error happens")
    //...
    log(0, "somework finished")
  }

def worker = new LoggingWorker()
 worker.log = {step,text -> println(['INFO','ERROR'][step]+': '+text) }
 worker.somework();

Solche Konstrukte vermeiden auch zunehmende Namenskonflikte, da die Closure ja intern symbolisiert ist. Nach all dem Staunen stellt sich die Frage nach Nachteilen, und die gibt es in der Tat: Seiteneffekte. Wer nicht diszipliniert arbeitet, dem spielen die upvalues (externe lokale Variablen) böse Streiche. Was auch erstaunt, die komplette Übergabe einer Closure gleich als Definition (Listing 7). Als Gag habe ich noch versucht, eine Closure zu visualisieren (Listing 8), sodass der ganze Monitor einfach zur grauen Fläche mutiert, hinter der Fläche sind die symbolischen Objekte, auf der Fläche die Funktion, und die Fläche selbst dient als Closure (die Fläche kann man mit F4 wieder verlassen). Für Java-Entwickler interessant sind die Diskussionen, ob mit Closures die Sprache oder die Java VM selbst erweitert werden soll. Hierbei handelt es sich um eine Binärversion des Kompromissvorschlags für die Unterstützung von Closures in Java (aka BGGA). Am besten gleich heute ein Non-Disclosure Statement unterschreiben.

     
with TMyClass.Create do begin //Delphi 2010
  DoWork(
     procedure(value: string)
     begin 
       Writeln(value);
     end);
  Free;
end;
//Object Pascal
  with TForm.Create(self) do begin
    BorderStyle:= bsNone;
    WindowState:= wsMaximized;
    Show;
  end;

Links & Literatur
[1] http://www.javac.info/
[2] http://sourceforge.net/projects/maxbox/
[3]
Michael Bolin: „Closure: The Definitive Guide“, O’Reilly Media; Auflage: 1 (20. Oktober 2010)

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -