Closure (Funktion)

QS-Informatik
Beteilige dich an der Diskussion!
Dieser Artikel wurde wegen inhaltlicher Mängel auf der Qualitätssicherungsseite der Redaktion Informatik eingetragen. Dies geschieht, um die Qualität der Artikel aus dem Themengebiet Informatik auf ein akzeptables Niveau zu bringen. Hilf mit, die inhaltlichen Mängel dieses Artikels zu beseitigen, und beteilige dich an der Diskussion! ()


Begründung: Siehe Diskussionsseite --Pjacobi 23:20, 16. Mär. 2009 (CET)

Als Closure oder Funktionsabschluss bezeichnet man eine Programmfunktion, die beim Aufruf einen Teil ihres vorherigen Aufrufkontexts reproduziert, selbst wenn dieser Kontext außerhalb der Funktion schon nicht mehr existiert. Closures „konservieren“ also ihren Kontext.

Closures sind ein Konzept, das aus den funktionalen Programmiersprachen stammt und zum ersten mal in LISP auftrat und in seinem Dialekt Scheme zum ersten mal vollständig unterstützt wurde. Daraufhin wurde es auch in den meisten späteren funktionalen Programmiersprachen (etwa Haskell, Ocaml) unterstützt.

Allerdings existieren auch nicht-funktionale Programmiersprachen, die diese Funktion unterstützen. Hier wären Delphi[1][2], JavaScript[3], Smalltalk, Python, Lua, Ruby, C#, Groovy, PHP und Perl zu nennen, das im Weiteren zur Veranschaulichung dient. Auch die objektorientierte Programmiersprache Java wird ab JDK 7 voraussichtlich eine Form von Funktionsabschlüssen unterstützen.[4] In C kann der gleiche Effekt durch Kopieren in statische Variablen erreicht werden. Apple hat den gcc und Clang um Closures – genannt Block-Literals – für C erweitert und dies zur Standardisierung vorgeschlagen.[5]

Der Kontext eines beliebigen Code-Fragments wird unter anderem durch die zur Verfügung stehenden Symbole bestimmt:

 # pragma
 use strict;
 
 # function
 sub function
  {
   # function vars
   my ($var1, $var2);
    
   # code
   ...
   
   if (<condition>)
    {
     # block vars
     my ($scopy1, $scopy2);
     
     # block code
     ...
    }
   
   # code
   ...
  }

Im oben gezeigten Beispiel sind die Variablen $var1 und $var2 an jeder Stelle der Funktion gültig und sichtbar, $scopy1 und $scopy2 jedoch nur innerhalb des Blocks, in dem sie definiert wurden. Beim Verlassen dieses Bereichs (ihres Gültigkeitsbereiches oder Scopes) 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;
 
 # function refs
 my ($c1);
 
 # function
 sub function
  {
   # function vars
   my ($var1, $var2);
    
   # code
   ...
   
   if (<condition>)
    {
     # block vars
     my ($scopy1, $scopy2);
     
     # block code
     $scopy1=10;
     $scopy2=1000;
     
     # define a closure
     $c1=sub {print "Scopies: $scopy1, $scopy2.\n"};
    }
   
   # code
   ...
  }

Das Laufzeitsystem stellt jetzt beim Aufräumen des if-Blocks fest, dass noch Referenzen auf die Blockvariablen $scopy1 und $scopy2 bestehen – die weiter gültige Variable $c1 verweist auf eine anonyme Subroutine, die ihrerseits Verweise auf die Blockvariablen enthält. $scopy1 und $scopy2 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 $c1->() ausführen und wird im Ergebnis immer wieder die bei der Definition der Funktion gültigen Werte der Variablen angezeigt bekommen.

Ä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 refs
 my ($printer, $incrementor, $decrementor);
 
 # function
 sub function
  {
   # function vars
   my ($var1, $var2);
   
   # code
   # ...
   
   if (0==0)
    {
     # block vars
     my ($scopy1, $scopy2);
     
     # block code
     $scopy1=10;
     $scopy2=1000;
     
     # define closures
     $printer=sub {print "Scopies: $scopy1, $scopy2.\n"};
     $incrementor=sub {$scopy1++; $scopy2++;};
     $decrementor=sub {$scopy1--; $scopy2--;};
    }
   
   # code
   # ...
  }
 
 # call the function
 &function;
 # use closures
 $printer->();
 $incrementor->();
 $printer->();
 $incrementor->();
 $incrementor->();
 $printer->();

Closures lassen sich also beispielsweise dazu verwenden, den Zugriff auf sensible Daten zu kapseln.

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 ihren 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.

Das Closure im vorigen Beispiel wird dann auf die folgende Weise verwendet:

>>> i, g = closure()
>>> g()
0
>>> i()
>>> i()
>>> print g()
2
>>> container
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: name 'container' is not defined

OCaml, als funktionelle Sprache, 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 einem Integer können natürlich auf diese Weise beliebige Objekte oder Variablen beliebiger Typen gekapselt werden.

Gegenüberstellung dynamischer und lexikalischer Gültigkeitsbereiche

Um lexikalische Funktionsabschlüsse und deren Sinn wirklich zu begreifen ist es hilfreich, lexikalischen Skopus mit dynamischem zu vergleichen. Die folgenden Beispiele sind in Common Lisp gehalten, da diese Sprache es erlaubt, sowohl Bindungen mit dynamischem, als auch lexikalischem Skopus und Closures zu verwenden.

In älteren Lisp-Dialekten (z. B. Maclisp, aber auch heutzutage noch in Emacs Lisp) war dynamische Bindung die Norm. Die Bindung einer freien dynamischen (nicht gebundenen) Variable wird durch den Call-Stack bestimmt. Das heißt: Wenn eine Funktion foo die freie Variable *x* (Die sogenannten "Earmuffs", also das einschließen des Bezeichners in * ist Konvention für global dynamische Variablen, sogenannte "special variables") verwendet, ohne sie zu binden, wird die nächste Bindung in der Kette der Funktionsaufrufe aufwärts verwandt.

CL-USER> (defparameter *x* 5) ; *x* wird durch defparameter global dynamisch
*x*
CL-USER> (defun foo () *x*) ; *x* ist hier ungebunden, also frei
FOO
CL-USER> (foo) ; der Aufruf von foo liefert den Wert der nächsten
5 ; Bindung von *x*
CL-USER> (let ((*x* 10)) ; *x* wird neu gebunden und 10 zugewiesen
           (defun foo ()
             *x*))
FOO
CL-USER> (foo) ; diese neue Bindung existiert hier nicht mehr, wieder
5 ; wird der Wert der globalen Bindung zurückgegeben
CL-USER> (let ((*x* 10)) ; hier wird foo innerhalb einer neuen Bindung aufgerufen
           (foo))
10

Problematisch ist, dass sich nicht anhand der textuellen Definition erkennen lässt, welche Bindung einer dynamischen Variable während des Aufrufs aktuell sein wird. Es bleibt nichts anderes, als die Kette der Funktionsaufrufe nach oben zurückzuverfolgen und die nächste Bindung ausfindig zu machen. Dies erschwert das Verständnis des Codes und kann schwer zu behebende Fehler verursachen.

CL-USER> (defun foo ()
           (let ((*x* 10))     ; *x* wird neu gebunden, 10 zugewiesen
             (bar)             ; bar wird aufgerufen
             *x*))             ; der Wert Bindung von *x* ist Rückgabewert
FOO
CL-USER> (defun bar ()
           (setq *x* 20))      ; der (in der Kette der Aufrufe) nächsten Bindung von *x* 
BAR ; wird 20 zugewiesen
CL-USER> (foo)
20

Hier hat also bar die Bindung von *x* einer im Call-Stack höheren, also aufrufenden, Funktion foo verändert. Dies stellt eine tückische Fehlerquelle dar.

Trotzdem können sich dynamische Bindungen in bestimmten Situationen als durchaus nützlich erweisen. So werden dynamische Variablen häufig verwendet, um das Verhalten aufgerufener Funktionen zu verändern, ohne weitere Parameter übergeben zu müssen.

CL-USER> (setq *print-length* 10)
10
CL-USER> '#1=(t . #1#) ; durch Zuweisung an die global dynamische Variable *print-length*, 
(T T T T T T T T T T ...) ; welche Einfluss auf die Ausgabefunktionen nimmt, wird diese
                           ; zirkuläre Liste nur bis zum zehnten Element ausgegeben

Im Gegensatz zu diesem Verhalten steht die lexikalische Bindung. Die aktuell gültige Bindung einer lexikalischen Variablen wird nicht durch die Kette der Funktionsaufrufe, sondern durch die sie textuell umgebenden Definitionen, ihren lexikalischen Kontext, bestimmt. Dies ermöglicht es, auf Anhieb anhand des Quelltextes die jeweils gültige Bindung einer Variablen zu bestimmen, noch bevor das Programm ausgeführt wird. (Deshalb auch die alternative Bezeichnung "Statischer Skopus")

CL-USER> (let ((x 10)) ; x wird lexikalisch gebunden
           (defun foo ()
             x))
FOO
CL-USER> (foo) ; foo liefert den Wert der Bindung von x, welche ihre
10 ; Definition textuell umgibt zurück
CL-USER> (let ((x 20))
           (foo))
10

Die oben aufgezeigte Problematik des Zuweisens an Bindungen einer Variablen aufrufender Funktionen ist damit hinfällig, da diese Bindung innerhalb der aufgerufenen Funktion nicht sichtbar ist.

CL-USER> (defun foo ()
           (let ((x 10))
             (bar)
             x))
FOO
CL-USER> (let ((x 15)) ; x wird hier durch let gebunden, da in CL keine globalen lexikalischen
           (defun bar ()    ; Bindungen existieren. Auch wenn der Sprachstandard dies vorsähe,
             (setq x 20)))  ; würde es das Ergebnis nicht beeinflussen
BAR
CL-USER> (foo)
10

Lexikalische Closures zu verstehen ist nun ein Leichtes: Der lexikalische Kontext einer Funktion wird auch dann bewahrt, wenn die Funktion außerhalb der sie textuell umgebenden Bindungen aufgerufen wird. Das heißt, dass beispielsweise eine anonyme Funktion, in Lisp Lambda-Ausdruck genannt, alle Bindungen der in ihr verwandten freien Variablen, welche sie in ihrer textuellen Definition umgeben, mit sich trägt.

CL-USER> (let ((x 10)) ; der Lambda-Ausdruck wird in einem lexikalischen Kontext
           (lambda () x))   ; definiert, in welchem eine Bindung für die Variable x besteht,
#<LEXICAL CLOSURE ...> ; welcher der Wert 10 zugewiesen ist
CL-USER> (funcall *) ; diese anonyme Funktion wird nun aufgerufen (* enthält den Rückgabewert
10 ; des letzten Ausdrucks, also die anonyme Funktion)

So lässt sich dann unter Verwendung lexikalischer Funktionsabschlüsse Verkapselung realisieren und auch das häufig zitierte Zähler-Beispiel:

CL-USER> (defun make-counter (init)
           (let ((n init))
             (lambda (&optional (action 'val))
               (ecase action
                 (val n)
                 (reset (setq n init))
                 (inc (incf n))
                 (dec (decf n))))))
MAKE-COUNTER
CL-USER> (setq *counter* (make-counter 5))
#<COMPILED-LEXICAL-CLOSURE (:INTERNAL MAKE-COUNTER) #x8FB3CBE>
CL-USER> (funcall *counter*)
5
CL-USER> (funcall *counter* 'inc)
6
CL-USER> (funcall *counter* 'inc)
7
CL-USER> (funcall *counter* 'dec)
6
CL-USER> (funcall *counter* 'reset)
5

Literatur

Einzelnachweise

  1. Craig Stuntz: Understanding Anonymous Methods
  2. Barry Kelly: Tiburon: fun with generics and anonymous methods
  3. Closures in Javascript (englisch)
  4. (Simplified) closures for the Java Programming Languag (v0.6a) (englischsprachig)
  5. N1370: Apple’s Extensions to C