Schließung (Computerprogrammierung) – Wikipedia

before-content-x4

In Programmiersprachen a Schließung, ebenfalls lexikalischer Verschluss oder Funktionsschlussist eine Technik zum Implementieren der lexikalischen Namensbindung in einer Sprache mit erstklassigen Funktionen. Operativ ist ein Abschluss ein Datensatz, in dem eine Funktion gespeichert ist[a] zusammen mit einer Umgebung.[1] Die Umgebung ist eine Zuordnung, die jede freie Variable der Funktion (Variablen, die lokal verwendet, aber in einem umschließenden Bereich definiert werden) mit dem Wert oder der Referenz verknüpft, an die der Name beim Erstellen des Abschlusses gebunden wurde.[b] Im Gegensatz zu einer einfachen Funktion ermöglicht ein Verschluss der Funktion den Zugriff auf diese erfasste Variablen durch die Kopien ihrer Werte oder Referenzen des Abschlusses, selbst wenn die Funktion außerhalb ihres Gültigkeitsbereichs aufgerufen wird.

Geschichte und Etymologie[edit]

Das Konzept der Verschlüsse wurde in den 1960er Jahren für die mechanische Bewertung von Ausdrücken im λ-Kalkül entwickelt und 1970 erstmals vollständig als Sprachmerkmal in der Programmiersprache PAL implementiert, um lexikalisch ausgerichtete erstklassige Funktionen zu unterstützen.[2]

Peter J. Landin definierte den Begriff Schließung im Jahr 1964 als mit einem Umwelt Teil und ein Steuerteil wie von seiner SECD-Maschine zur Auswertung von Ausdrücken verwendet.[3]Joel Moses schreibt Landin die Einführung des Begriffs zu Schließung sich auf einen Lambda-Ausdruck zu beziehen, dessen offene Bindungen (freie Variablen) durch die lexikalische Umgebung geschlossen (oder in diese gebunden) wurden, was zu a führt geschlossener Ausdruckoder Schließung.[4][5] Diese Verwendung wurde später von Sussman und Steele übernommen, als sie 1975 das Schema definierten.[6] eine lexikalisch begrenzte Variante von LISP und verbreitete sich.

Anonyme Funktionen[edit]

Der Begriff Schließung wird oft als Synonym für anonyme Funktion verwendet, obwohl eine anonyme Funktion streng genommen ein Funktionsliteral ohne Namen ist, während ein Abschluss eine Instanz einer Funktion ist, ein Wert, dessen nicht-lokale Variablen entweder an Werte oder an gebunden sind Speicherorte (abhängig von der Sprache; siehe Abschnitt über die lexikalische Umgebung unten).

Zum Beispiel im folgenden Python-Code:

def f(x):
    def g(y):
        return x + y
    return g  # Return a closure.

def h(x):
    return lambda y: x + y  # Return a closure.

# Assigning specific closures to variables.
a = f(1)
b = h(1)

# Using the closures stored in variables.
assert a(5) == 6
assert b(5) == 6

# Using closures without binding them to variables first.
assert f(1)(5) == 6  # f(1) is the closure.
assert h(1)(5) == 6  # h(1) is the closure.

die Werte von a und b sind Abschlüsse, die in beiden Fällen durch Rückgabe einer verschachtelten Funktion mit einer freien Variablen aus der umschließenden Funktion erzeugt werden, so dass die freie Variable an den Wert des Parameters bindet x der umschließenden Funktion. Die Verschlüsse in a und b sind funktional identisch. Der einzige Unterschied bei der Implementierung besteht darin, dass wir im ersten Fall eine verschachtelte Funktion mit einem Namen verwendet haben. gIm zweiten Fall haben wir eine anonyme verschachtelte Funktion verwendet (mit dem Schlüsselwort Python lambda zum Erstellen einer anonymen Funktion). Der ursprüngliche Name, falls vorhanden, um sie zu definieren, ist irrelevant.

Ein Abschluss ist ein Wert wie jeder andere Wert. Es muss keiner Variablen zugewiesen werden und kann stattdessen direkt verwendet werden, wie in den letzten beiden Zeilen des Beispiels gezeigt. Diese Nutzung kann als “anonyme Schließung” angesehen werden.

Die verschachtelten Funktionsdefinitionen sind selbst keine Abschlüsse: Sie haben eine freie Variable, die noch nicht gebunden ist. Erst wenn die einschließende Funktion mit einem Wert für den Parameter ausgewertet wird, wird die freie Variable der verschachtelten Funktion gebunden, wodurch ein Abschluss erstellt wird, der dann von der einschließenden Funktion zurückgegeben wird.

Schließlich unterscheidet sich ein Abschluss nur dann von einer Funktion mit freien Variablen, wenn er außerhalb des Bereichs der nicht lokalen Variablen liegt. Andernfalls stimmen die definierende Umgebung und die Ausführungsumgebung überein, und es gibt keine Unterscheidung zwischen diesen (statische und dynamische Bindung können nicht unterschieden werden, weil Die Namen werden in dieselben Werte aufgelöst. Funktioniert im folgenden Programm beispielsweise mit einer freien Variablen x (gebunden an die nicht lokale Variable x mit globalem Geltungsbereich) werden in derselben Umgebung ausgeführt, in der x definiert ist, so ist es unerheblich, ob es sich tatsächlich um Schließungen handelt:

x = 1
nums = [1, 2, 3]

def f(y):
    return x + y

map(f, nums)
map(lambda y: x + y, nums)

Dies wird meistens durch eine Funktionsrückgabe erreicht, da die Funktion im Bereich der nicht lokalen Variablen definiert werden muss. In diesem Fall ist ihr eigener Bereich normalerweise kleiner.

Dies kann auch durch variable Abschattung erreicht werden (was den Umfang der nicht lokalen Variablen verringert), obwohl dies in der Praxis weniger häufig ist, da es weniger nützlich ist und von Abschattung abgeraten wird. In diesem Beispiel f kann als Schließung gesehen werden, weil x im Körper von f ist an die gebunden x im globalen Namespace, nicht im x lokal zu g::

x = 0

def f(y):
    return x + y

def g(z):
    x = 1  # local x shadows global x
    return f(z)

g(1)  # evaluates to 1, not 2

Anwendungen[edit]

Die Verwendung von Verschlüssen ist mit Sprachen verbunden, in denen Funktionen erstklassige Objekte sind, in denen Funktionen als Ergebnis von Funktionen höherer Ordnung zurückgegeben oder als Argumente an andere Funktionsaufrufe übergeben werden können. Wenn Funktionen mit freien Variablen erstklassig sind, führt die Rückgabe einer Funktion zu einem Abschluss. Dies umfasst funktionale Programmiersprachen wie Lisp und ML sowie viele moderne Multi-Paradigmen-Sprachen wie Python und Rust. Abschlüsse werden auch häufig bei Rückrufen verwendet, insbesondere für Ereignishandler, z. B. in JavaScript, wo sie für Interaktionen mit einer dynamischen Webseite verwendet werden.

Verschlüsse können auch in einem fortlaufenden Stil verwendet werden, um den Status auszublenden. Konstrukte wie Objekte und Kontrollstrukturen können somit mit Verschlüssen implementiert werden. In einigen Sprachen kann ein Abschluss auftreten, wenn eine Funktion innerhalb einer anderen Funktion definiert ist und die innere Funktion sich auf lokale Variablen der äußeren Funktion bezieht. Zur Laufzeit, wenn die äußere Funktion ausgeführt wird, wird ein Abschluss gebildet, der aus dem Code der inneren Funktion und Verweisen (den Aufwärtswerten) auf alle Variablen der äußeren Funktion besteht, die für den Abschluss erforderlich sind.

Erstklassige Funktionen[edit]

Abschlüsse werden normalerweise in Sprachen mit erstklassigen Funktionen angezeigt. Mit anderen Worten, solche Sprachen ermöglichen die Übergabe von Funktionen als Argumente, die von Funktionsaufrufen zurückgegeben, an Variablennamen usw. gebunden werden, genau wie einfachere Typen wie Zeichenfolgen und Ganzzahlen. Betrachten Sie beispielsweise die folgende Schemafunktion:

; Return a list of all books with at least THRESHOLD copies sold.
(define (best-selling-books threshold)
  (filter
    (lambda (book)
      (>= (book-sales book) threshold))
    book-list))

In diesem Beispiel der Lambda-Ausdruck (lambda (book) (>= (book-sales book) threshold)) erscheint innerhalb der Funktion best-selling-books. Wenn der Lambda-Ausdruck ausgewertet wird, erstellt Scheme einen Abschluss, der aus dem Code für den Lambda-Ausdruck und einem Verweis auf den besteht threshold Variable, die eine freie Variable innerhalb des Lambda-Ausdrucks ist.

Der Verschluss wird dann an die übergeben filter Funktion, die es wiederholt aufruft, um zu bestimmen, welche Bücher zur Ergebnisliste hinzugefügt und welche verworfen werden sollen. Weil der Verschluss selbst einen Verweis auf hat thresholdkann diese Variable jedes Mal verwenden filter nennt es. Die Funktion filter selbst könnte in einer völlig separaten Datei definiert werden.

Hier ist das gleiche Beispiel, das in JavaScript, einer anderen beliebten Sprache mit Unterstützung für Schließungen, neu geschrieben wurde:

// Return a list of all books with at least 'threshold' copies sold.
function bestSellingBooks(threshold) {
  return bookList.filter(
      function (book) { return book.sales >= threshold; }
    );
}

Das function Schlüsselwort wird hier anstelle von verwendet lambda, und ein Array.filter Methode[7] anstelle eines globalen filter Funktion, aber ansonsten sind die Struktur und die Wirkung des Codes gleich.

Eine Funktion kann einen Abschluss erstellen und wie im folgenden Beispiel zurückgeben:

// Return a function that approximates the derivative of f
// using an interval of dx, which should be appropriately small.
function derivative(f, dx) {
  return function (x) {
    return (f(x + dx) - f(x)) / dx;
  };
}

Da der Abschluss in diesem Fall die Ausführung der Funktion, die ihn erstellt, überlebt, sind die Variablen f und dx lebe weiter nach der Funktion derivative kehrt zurück, obwohl die Ausführung ihren Gültigkeitsbereich verlassen hat und sie nicht mehr sichtbar sind. In Sprachen ohne Abschlüsse fällt die Lebensdauer einer automatischen lokalen Variablen mit der Ausführung des Stapelrahmens zusammen, in dem diese Variable deklariert ist. In Sprachen mit Abschlüssen müssen Variablen weiterhin vorhanden sein, solange vorhandene Abschlüsse Verweise darauf haben. Dies wird am häufigsten mithilfe einer Speicherbereinigung implementiert.

Staatsvertretung[edit]

Ein Abschluss kann verwendet werden, um eine Funktion mit einer Reihe von “privaten” Variablen zu verknüpfen, die über mehrere Aufrufe der Funktion bestehen bleiben. Der Bereich der Variablen umfasst nur die Closed-Over-Funktion, sodass von keinem anderen Programmcode aus darauf zugegriffen werden kann. Diese sind analog zu privaten Variablen in der objektorientierten Programmierung, und tatsächlich sind Abschlüsse analog zu einem Objekttyp, insbesondere Funktionsobjekten, mit einer einzigen öffentlichen Methode (Funktionsaufruf) und möglicherweise vielen privaten Variablen (den gebundenen Variablen).

In zustandsbehafteten Sprachen können Schließungen daher verwendet werden, um Paradigmen für die Darstellung von Zuständen und das Verbergen von Informationen zu implementieren, da die Aufwärtswerte der Schließung (ihre geschlossenen Variablen) von unbestimmter Ausdehnung sind, sodass ein in einem Aufruf festgelegter Wert im nächsten verfügbar bleibt. Auf diese Weise verwendete Verschlüsse haben keine referenzielle Transparenz mehr und sind daher keine reinen Funktionen mehr. Dennoch werden sie häufig in unreinen Funktionssprachen wie Scheme verwendet.

Andere Verwendungen[edit]

Verschlüsse haben viele Verwendungszwecke:

  • Da Schließungen die Auswertung verzögern, dh nichts “tun”, bis sie aufgerufen werden, können sie zum Definieren von Kontrollstrukturen verwendet werden. Beispielsweise werden alle Standardsteuerungsstrukturen von Smalltalk, einschließlich Zweigen (if / then / else) und Schleifen (while und for), mithilfe von Objekten definiert, deren Methoden Abschlüsse akzeptieren. Benutzer können auch einfach ihre eigenen Kontrollstrukturen definieren.
  • In Sprachen, die Zuweisungen implementieren, können mehrere Funktionen erstellt werden, die sich in derselben Umgebung befinden, sodass sie durch Ändern dieser Umgebung privat kommunizieren können. Im Schema:
(define foo #f)
(define bar #f)

(let ((secret-message "none"))
  (set! foo (lambda (msg) (set! secret-message msg)))
  (set! bar (lambda () secret-message)))

(display (bar)) ; prints "none"
(newline)
(foo "meet me by the docks at midnight")
(display (bar)) ; prints "meet me by the docks at midnight"
  • Verschlüsse können verwendet werden, um Objektsysteme zu implementieren.[8]

Hinweis: Einige Sprecher bezeichnen jede Datenstruktur, die eine lexikalische Umgebung bindet, als Abschluss. Der Begriff bezieht sich jedoch normalerweise speziell auf Funktionen.

Implementierung und Theorie[edit]

Abschlüsse werden normalerweise mit einer speziellen Datenstruktur implementiert, die einen Zeiger auf den Funktionscode sowie eine Darstellung der lexikalischen Umgebung der Funktion (dh des Satzes verfügbarer Variablen) zum Zeitpunkt der Erstellung des Abschlusses enthält. Die referenzierende Umgebung bindet die nicht lokalen Namen zum Zeitpunkt der Erstellung des Abschlusses an die entsprechenden Variablen in der lexikalischen Umgebung und verlängert ihre Lebensdauer zusätzlich auf mindestens die Lebensdauer des Abschlusses. Wenn der Abschluss zu einem späteren Zeitpunkt eingegeben wird, möglicherweise in einer anderen lexikalischen Umgebung, wird die Funktion mit ihren nicht lokalen Variablen ausgeführt, die sich auf die vom Abschluss erfassten Variablen beziehen, nicht auf die aktuelle Umgebung.

Eine Sprachimplementierung kann vollständige Schließungen nicht einfach unterstützen, wenn ihr Laufzeitspeichermodell alle automatischen Variablen auf einem linearen Stapel zuweist. In solchen Sprachen werden die automatischen lokalen Variablen einer Funktion freigegeben, wenn die Funktion zurückgegeben wird. Ein Abschluss erfordert jedoch, dass die freien Variablen, auf die er verweist, die Ausführung der einschließenden Funktion überleben. Daher müssen diese Variablen so zugewiesen werden, dass sie bestehen bleiben, bis sie nicht mehr benötigt werden, normalerweise über die Heap-Zuweisung, und nicht mehr auf dem Stapel. Ihre Lebensdauer muss so verwaltet werden, dass sie überleben, bis alle auf sie verweisenden Abschlüsse nicht mehr verwendet werden.

Dies erklärt, warum normalerweise Sprachen, die Schließungen nativ unterstützen, auch die Garbage Collection verwenden. Die Alternativen sind die manuelle Speicherverwaltung nicht lokaler Variablen (explizite Zuweisung auf dem Heap und Freigabe nach Abschluss) oder die Verwendung der Stapelzuweisung durch die Sprache, um zu akzeptieren, dass bestimmte Anwendungsfälle aufgrund baumelnder Zeiger auf zu undefiniertem Verhalten führen befreite automatische Variablen, wie in Lambda-Ausdrücken in C ++ 11[9] oder verschachtelte Funktionen in GNU C.[10] Das Funarg-Problem (oder “Funktionsargument” -Problem) beschreibt die Schwierigkeit, Funktionen als erstklassige Objekte in einer stapelbasierten Programmiersprache wie C oder C ++ zu implementieren. In ähnlicher Weise wird in D Version 1 davon ausgegangen, dass der Programmierer weiß, was mit Delegaten und automatischen lokalen Variablen zu tun ist, da ihre Referenzen nach Rückkehr aus seinem Definitionsbereich ungültig sind (automatische lokale Variablen befinden sich auf dem Stapel) – dies erlaubt immer noch viele nützliche Funktionsmuster, aber für komplexe Fälle erfordert explizite Heap-Zuordnung für Variablen. D Version 2 löste dieses Problem, indem erkannt wurde, welche Variablen auf dem Heap gespeichert werden müssen, und führte eine automatische Zuordnung durch. Da D die Garbage Collection verwendet, ist es in beiden Versionen nicht erforderlich, die Verwendung von Variablen bei der Übergabe zu verfolgen.

In streng funktionalen Sprachen mit unveränderlichen Daten (z.B Erlang) ist die automatische Speicherverwaltung (Garbage Collection) sehr einfach zu implementieren, da in den Variablenreferenzen keine möglichen Zyklen vorhanden sind. In Erlang werden beispielsweise alle Argumente und Variablen auf dem Heap zugewiesen, aber Verweise darauf werden zusätzlich auf dem Stapel gespeichert. Nach der Rückkehr einer Funktion sind die Referenzen weiterhin gültig. Die Heap-Reinigung erfolgt durch einen inkrementellen Garbage Collector.

In ML haben lokale Variablen einen lexikalischen Gültigkeitsbereich und definieren daher ein stapelartiges Modell. Da sie jedoch an Werte und nicht an Objekte gebunden sind, kann eine Implementierung diese Werte auf eine Weise in die Datenstruktur des Abschlusses kopieren, die für sie unsichtbar ist der Programmierer.

Scheme, das über ein ALGOL-ähnliches lexikalisches Scope-System mit dynamischen Variablen und Garbage Collection verfügt, verfügt nicht über ein Stack-Programmiermodell und leidet nicht unter den Einschränkungen stapelbasierter Sprachen. Verschlüsse werden natürlich im Schema ausgedrückt. Das Lambda-Formular enthält den Code, und die freien Variablen seiner Umgebung bleiben im Programm erhalten, solange auf sie möglicherweise zugegriffen werden kann, und können daher so frei wie jeder andere Scheme-Ausdruck verwendet werden.[citation needed]

Abschlüsse sind eng mit Akteuren im Actor-Modell der gleichzeitigen Berechnung verwandt, wobei die Werte in der lexikalischen Umgebung der Funktion aufgerufen werden Bekannte. Ein wichtiges Problem bei Schließungen in gleichzeitigen Programmiersprachen ist, ob die Variablen in einer Schließung aktualisiert werden können und wenn ja, wie diese Aktualisierungen synchronisiert werden können. Schauspieler bieten eine Lösung.[11]

Verschlüsse sind eng mit Funktionsobjekten verbunden. Die Umwandlung von ersteren zu letzteren wird als Defunktionalisierung oder Lambda-Heben bezeichnet. siehe auch Verschlussumwandlung.[citation needed]

Unterschiede in der Semantik[edit]

Lexikalische Umgebung[edit]

Da verschiedene Sprachen nicht immer eine gemeinsame Definition der lexikalischen Umgebung haben, können auch ihre Definitionen des Abschlusses variieren. Die allgemein verbreitete minimalistische Definition der lexikalischen Umgebung definiert sie als eine Menge aller Bindungen von Variablen im Bereich, und das ist auch das, was Abschlüsse in jeder Sprache erfassen müssen. Die Bedeutung einer variablen Bindung unterscheidet sich jedoch ebenfalls. In imperativen Sprachen binden Variablen an relative Speicherorte, an denen Werte gespeichert werden können. Obwohl sich die relative Position einer Bindung zur Laufzeit nicht ändert, kann sich der Wert an der gebundenen Position ändern. In solchen Sprachen wird, da das Schließen die Bindung erfasst, jede Operation an der Variablen, unabhängig davon, ob sie vom Schließen ausgeht oder nicht, an demselben relativen Speicherort ausgeführt. Dies wird oft als Erfassung der Variablen “als Referenz” bezeichnet. Hier ist ein Beispiel, das das Konzept in ECMAScript veranschaulicht, einer solchen Sprache:

// ECMAScript
var f, g;
function foo() {
  var x;
  f = function() { return ++x; };
  g = function() { return --x; };
  x = 1;
  alert('inside foo, call to f(): ' + f());
}
foo();  // 2
alert('call to g(): ' + g());  // 1 (--x)
alert('call to g(): ' + g());  // 0 (--x)
alert('call to f(): ' + f());  // 1 (++x)
alert('call to f(): ' + f());  // 2 (++x)

Funktion foo und die Verschlüsse, auf die sich Variablen beziehen f und g Alle verwenden denselben relativen Speicherort, der durch die lokale Variable gekennzeichnet ist x.

In einigen Fällen kann das obige Verhalten unerwünscht sein und es ist notwendig, einen anderen lexikalischen Verschluss zu binden. Auch in ECMAScript würde dies mit dem erfolgen Function.bind().

Beispiel 1: Verweis auf eine ungebundene Variable[edit]

[12]

var module = {
  x: 42,
  getX: function() {return this.x; }
}
var unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// emits undefined as 'x' is not specified in global scope.

var boundGetX = unboundGetX.bind(module); // specify object module as the closure
console.log(boundGetX()); // emits 42

Beispiel 2: Versehentlicher Verweis auf eine gebundene Variable[edit]

In diesem Beispiel würde das erwartete Verhalten darin bestehen, dass jeder Link beim Klicken seine ID ausgibt. Da die Variable ‘e’ jedoch an den oben genannten Bereich gebunden und beim Klicken verzögert ausgewertet wird, gibt jedes Ereignis beim Klicken die ID des letzten Elements in ‘elements’ aus, das am Ende der for-Schleife gebunden ist.[13]

var elements= document.getElementsByTagName('a');
//Incorrect: e is bound to the function containing the 'for' loop, not the closure of "handle"
for (var e in elements){ e.onclick=function handle(){ alert(e.id);} }

Auch hier variabel e müsste durch den Umfang des Blocks mit gebunden werden handle.bind(this) oder der let Stichwort.

Andererseits binden viele funktionale Sprachen wie ML Variablen direkt an Werte. In diesem Fall besteht keine Notwendigkeit, den Status zwischen Abschlüssen zu teilen, da es keine Möglichkeit gibt, den Wert der Variablen nach ihrer Bindung zu ändern. Sie verwenden lediglich dieselben Werte. Dies wird oft als Erfassung der Variablen “nach Wert” bezeichnet. Die lokalen und anonymen Klassen von Java fallen ebenfalls in diese Kategorie – sie erfordern erfasste lokale Variablen finalDies bedeutet auch, dass kein Status geteilt werden muss.

In einigen Sprachen können Sie wählen, ob Sie den Wert einer Variablen oder ihren Speicherort erfassen möchten. In C ++ 11 werden erfasste Variablen beispielsweise entweder mit deklariert [&], was bedeutet, durch Referenz oder mit erfasst [=], was bedeutet, durch Wert erfasst.

Eine weitere Untergruppe, faule Funktionssprachen wie Haskell, binden Variablen eher an Ergebnisse zukünftiger Berechnungen als an Werte. Betrachten Sie dieses Beispiel in Haskell:

-- Haskell
foo :: Fractional a => a -> a -> (a -> a)
foo x y = (z -> z + r)
          where r = x / y

f :: Fractional a => a -> a
f = foo 1 0

main = print (f 123)

Die Bindung von r erfasst durch den in der Funktion definierten Verschluss foo ist zur Berechnung (x / y)– was in diesem Fall zur Division durch Null führt. Da jedoch die Berechnung erfasst wird und nicht der Wert, tritt der Fehler nur dann auf, wenn der Abschluss aufgerufen wird, und versucht tatsächlich, die erfasste Bindung zu verwenden.

Schließung verlassen[edit]

Noch mehr Unterschiede manifestieren sich im Verhalten anderer Konstrukte mit lexikalischem Umfang, wie z return, break und continue Aussagen. Solche Konstrukte können im Allgemeinen als Aufruf einer Escape-Fortsetzung betrachtet werden, die durch eine einschließende Steueranweisung festgelegt wurde (im Fall von break und continueFür eine solche Interpretation müssen Schleifenkonstrukte in Bezug auf rekursive Funktionsaufrufe berücksichtigt werden. In einigen Sprachen, wie z. B. ECMAScript, return bezieht sich auf die Fortsetzung, die durch den lexikalisch innersten Verschluss in Bezug auf die Aussage hergestellt wird – also a return Innerhalb eines Abschlusses wird die Kontrolle an den Code übertragen, der sie aufgerufen hat. In Smalltalk jedoch der oberflächlich ähnliche Operator ^ Ruft die für den Methodenaufruf festgelegte Escape-Fortsetzung auf und ignoriert die Escape-Fortsetzungen aller dazwischen liegenden verschachtelten Abschlüsse. Die Escape-Fortsetzung eines bestimmten Abschlusses kann in Smalltalk nur implizit aufgerufen werden, wenn das Ende des Code des Abschlusses erreicht ist. Die folgenden Beispiele in ECMAScript und Smalltalk verdeutlichen den Unterschied:

"Smalltalk"
foo
  | xs |
  xs := #(1 2 3 4).
  xs do: [:x | ^x].
  ^0
bar
  Transcript show: (self foo printString) "prints 1"
// ECMAScript
function foo() {
  var xs = [1, 2, 3, 4];
  xs.forEach(function (x) { return x; });
  return 0;
}
alert(foo()); // prints 0

Die obigen Codefragmente verhalten sich anders als das Smalltalk ^ Operator und das JavaScript return Betreiber sind nicht analog. Im ECMAScript-Beispiel return x verlässt den inneren Verschluss, um eine neue Iteration des zu beginnen forEach Schleife, während im Smalltalk-Beispiel ^x bricht die Schleife ab und kehrt von der Methode zurück foo.

Common Lisp bietet ein Konstrukt, das eine der oben genannten Aktionen ausdrücken kann: Lisp (return-from foo x) verhält sich wie Smalltalk ^xwährend Lisp (return-from nil x) verhält sich wie JavaScript return x. Smalltalk ermöglicht es daher, dass eine erfasste Escape-Fortsetzung das Ausmaß überlebt, in dem sie erfolgreich aufgerufen werden kann. Erwägen:

"Smalltalk"
foo
    ^[ :x | ^x ]
bar
    | f |
    f := self foo.
    f value: 123 "error!"

Wenn der Verschluss von der Methode zurückgegeben wird foo Wenn es aufgerufen wird, versucht es, einen Wert aus dem Aufruf von zurückzugeben foo das schuf die Schließung. Da dieser Aufruf bereits zurückgegeben wurde und das Aufrufmodell der Smalltalk-Methode nicht der Spaghetti-Stack-Disziplin folgt, um mehrere Rückgaben zu ermöglichen, führt dieser Vorgang zu einem Fehler.

Einige Sprachen, wie z. B. Ruby, ermöglichen es dem Programmierer, den Weg zu wählen return ist gefangen. Ein Beispiel in Ruby:

# Ruby

# Closure using a Proc
def foo
  f = Proc.new { return "return from foo from inside proc" }
  f.call # control leaves foo here
  return "return from foo"
end

# Closure using a lambda
def bar
  f = lambda { return "return from lambda" }
  f.call # control does not leave bar here
  return "return from bar"
end

puts foo # prints "return from foo from inside proc"
puts bar # prints "return from bar"

Beide Proc.new und lambda In diesem Beispiel gibt es Möglichkeiten, einen Abschluss zu erstellen, aber die Semantik der so erstellten Abschlüsse unterscheidet sich in Bezug auf die return Erklärung.

In Schema, Definition und Umfang der return Die Steueranweisung ist explizit (und wird im Beispiel nur willkürlich als “return” bezeichnet). Das Folgende ist eine direkte Übersetzung des Ruby-Beispiels.

; Scheme
(define call/cc call-with-current-continuation)

(define (foo)
  (call/cc
   (lambda (return)
     (define (f) (return "return from foo from inside proc"))
     (f) ; control leaves foo here
     (return "return from foo"))))

(define (bar)
  (call/cc
   (lambda (return)
     (define (f) (call/cc (lambda (return) (return "return from lambda"))))
     (f) ; control does not leave bar here
     (return "return from bar"))))

(display (foo)) ; prints "return from foo from inside proc"
(newline)
(display (bar)) ; prints "return from bar"

Verschlussartige Konstrukte[edit]

Einige Sprachen verfügen über Funktionen, die das Verhalten von Schließungen simulieren. In Sprachen wie Java, C ++, Objective-C, C #, VB.NET und D sind diese Funktionen das Ergebnis des objektorientierten Paradigmas der Sprache.

Rückrufe (C)[edit]

Einige C-Bibliotheken unterstützen Rückrufe. Dies wird manchmal implementiert, indem beim Registrieren des Rückrufs bei der Bibliothek zwei Werte angegeben werden: ein Funktionszeiger und ein separater void* Zeiger auf beliebige Daten nach Wahl des Benutzers. Wenn die Bibliothek die Rückruffunktion ausführt, leitet sie den Datenzeiger weiter. Auf diese Weise kann der Rückruf den Status beibehalten und auf Informationen verweisen, die zum Zeitpunkt der Registrierung in der Bibliothek erfasst wurden. Die Redewendung ähnelt Verschlüssen in der Funktionalität, jedoch nicht in der Syntax. Das
void* Der Zeiger ist nicht typsicher, daher unterscheidet sich dieses C-Idiom von typsicheren Verschlüssen in C #, Haskell oder ML.

Rückrufe werden häufig in GUI-Widget-Toolkits verwendet, um die ereignisgesteuerte Programmierung zu implementieren, indem allgemeine Funktionen grafischer Widgets (Menüs, Schaltflächen, Kontrollkästchen, Schieberegler, Drehfelder usw.) mit anwendungsspezifischen Funktionen verknüpft werden, die das gewünschte spezifische Verhalten für die Anwendung implementieren.

Verschachtelte Funktion und Funktionszeiger (C)[edit]

Mit einer gcc-Erweiterung a verschachtelte Funktion kann verwendet werden und ein Funktionszeiger kann Schließungen emulieren, vorausgesetzt, die Funktion verlässt den enthaltenen Bereich nicht. Das folgende Beispiel ist ungültig, weil adder ist eine Definition der obersten Ebene (abhängig von der Compilerversion kann sie zu einem korrekten Ergebnis führen, wenn sie ohne Optimierung kompiliert wird, dh bei -O0):

#include 

typedef int (*fn_int_to_int)(int); // type of function int->int

fn_int_to_int adder(int number) {
  int add (int value) { return value + number; }
  return &add; // & operator is optional here because the name of a function in C is a pointer pointing on itself
}

int main(void) {
  fn_int_to_int add10 = adder(10);
  printf("%dn", add10(1));
  return 0;
}

Aber sich bewegen adder (und optional die typedef) im main macht es gültig:

#include 

int main(void) {
  typedef int (*fn_int_to_int)(int); // type of function int->int
  
  fn_int_to_int adder(int number) {
    int add (int value) { return value + number; }
    return add;
  }
  
  fn_int_to_int add10 = adder(10);
  printf("%dn", add10(1));
  return 0;
}

Wenn ausgeführt, wird dies jetzt gedruckt 11 wie erwartet.

Lokale Klassen und Lambda-Funktionen (Java)[edit]

Mit Java können Klassen innerhalb von Methoden definiert werden. Diese nennt man lokale Klassen. Wenn solche Klassen nicht benannt sind, werden sie als bezeichnet anonyme Klassen (oder anonym innere Klassen). Eine lokale Klasse (entweder benannt oder anonym) kann sich auf Namen in lexikalisch einschließenden Klassen oder auf schreibgeschützte Variablen (markiert als) beziehen final) in der lexikalisch einschließenden Methode.

class CalculationWindow extends JFrame {
    private volatile int result;
    ...
    public void calculateInSeparateThread(final URI uri) {
        // The expression "new Runnable() { ... }" is an anonymous class implementing the 'Runnable' interface.
        new Thread(
            new Runnable() {
                void run() {
                    // It can read final local variables:
                    calculate(uri);
                    // It can access private fields of the enclosing class:
                    result = result + 10;
                }
            }
        ).start();
    }
}

Die Erfassung von final Mit Variablen können Sie Variablen nach Wert erfassen. Auch wenn die Variable, die Sie erfassen möchten, nichtfinalkönnen Sie es jederzeit in ein temporäres kopieren final Variable kurz vor der Klasse.

Das Erfassen von Variablen als Referenz kann mithilfe von a emuliert werden final Verweis auf einen veränderlichen Container, z. B. ein Einzelelementarray. Die lokale Klasse kann den Wert der Containerreferenz selbst nicht ändern, aber den Inhalt des Containers.

Mit dem Aufkommen der Lambda-Ausdrücke von Java 8[14] Durch das Schließen wird der obige Code wie folgt ausgeführt:

class CalculationWindow extends JFrame {
    private volatile int result;
    ...
    public void calculateInSeparateThread(final URI uri) {
        // The code () -> { /* code */ } is a closure.
        new Thread(() -> {
                calculate(uri);
                result = result + 10;
        }).start();
    }
}

Lokale Klassen sind eine der Arten innerer Klassen, die im Hauptteil einer Methode deklariert werden. Java unterstützt auch innere Klassen, die als deklariert sind nicht statische Mitglieder einer einschließenden Klasse.[15] Sie werden normalerweise nur als “innere Klassen” bezeichnet.[16] Diese werden im Hauptteil der einschließenden Klasse definiert und haben vollen Zugriff auf Instanzvariablen der einschließenden Klasse. Aufgrund ihrer Bindung an diese Instanzvariablen kann eine innere Klasse nur mit einer expliziten Bindung an eine Instanz der einschließenden Klasse unter Verwendung einer speziellen Syntax instanziiert werden.[17]

public class EnclosingClass {
    /* Define the inner class */
    public class InnerClass {
        public int incrementAndReturnCounter() {
            return counter++;
        }
    }

    private int counter;
    {
        counter = 0;
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        EnclosingClass enclosingClassInstance = new EnclosingClass();
        /* Instantiate the inner class, with binding to the instance */
        EnclosingClass.InnerClass innerClassInstance =
            enclosingClassInstance.new InnerClass();

        for (int i = enclosingClassInstance.getCounter(); (i =
        innerClassInstance.incrementAndReturnCounter()) < 10;) {
            System.out.println(i);
        }
    }
}

Bei der Ausführung werden die Ganzzahlen von 0 bis 9 gedruckt. Achten Sie darauf, diesen Klassentyp nicht mit der verschachtelten Klasse zu verwechseln, die auf die gleiche Weise mit der begleitenden Verwendung des Modifikators “statisch” deklariert wird. Diese haben nicht den gewünschten Effekt, sondern sind nur Klassen ohne spezielle Bindung, die in einer umschließenden Klasse definiert sind.

Ab Java 8 unterstützt Java Funktionen als erstklassige Objekte. Lambda-Ausdrücke dieser Form gelten als vom Typ Function wobei T die Domäne und U der Bildtyp ist. Der Ausdruck kann mit seinem aufgerufen werden .apply(T t) Methode, aber nicht mit einem Standardmethodenaufruf.

public static void main(String[] args) {
    Function<String, Integer> length = s -> s.length();

    System.out.println( length.apply("Hello, world!") ); // Will print 13.
}

Blöcke (C, C ++, Objective-C 2.0)[edit]

Apple führte Blöcke, eine Form des Schließens, als nicht standardmäßige Erweiterung in C, C ++, Objective-C 2.0 und in Mac OS X 10.6 “Snow Leopard” und iOS 4.0 ein. Apple stellte ihre Implementierung für die GCC- und Clang-Compiler zur Verfügung.

Zeiger zum Blockieren und Blockieren von Literalen sind mit gekennzeichnet ^. Normale lokale Variablen werden beim Erstellen des Blocks nach Wert erfasst und sind innerhalb des Blocks schreibgeschützt. Variablen, die als Referenz erfasst werden sollen, sind mit gekennzeichnet __block. Blöcke, die außerhalb des Bereichs verbleiben müssen, in dem sie erstellt wurden, müssen möglicherweise kopiert werden.[18][19]

typedef int (^IntBlock)();

IntBlock downCounter(int start) {
	 __block int i = start;
	 return [[ ^int() {
		 return i--;
	 } copy] autorelease];
}

IntBlock f = downCounter(5);
NSLog(@"%d", f());
NSLog(@"%d", f());
NSLog(@"%d", f());

Delegierte (C #, VB.NET, D)[edit]

Anonyme C # -Methoden und Lambda-Ausdrücke unterstützen das Schließen:

var data = new[] {1, 2, 3, 4};
var multiplier = 2;
var result = data.Select(x => x * multiplier);

Visual Basic .NET, das viele ähnliche Sprachfunktionen wie C # bietet, unterstützt auch Lambda-Ausdrücke mit Abschlüssen:

Dim data = {1, 2, 3, 4}
Dim multiplier = 2
Dim result = data.Select(Function(x) x * multiplier)

In D werden Closures von Delegaten implementiert, einem Funktionszeiger, der mit einem Kontextzeiger gepaart ist (z. B. einer Klasseninstanz oder einem Stapelrahmen auf dem Heap bei Closures).

auto test1() {
    int a = 7;
    return delegate() { return a + 3; }; // anonymous delegate construction
}

auto test2() {
    int a = 20;
    int foo() { return a + 5; } // inner function
    return &foo;  // other way to construct delegate
}

void bar() {
    auto dg = test1();
    dg();    // =10   // ok, test1.a is in a closure and still exists

    dg = test2();
    dg();    // =25   // ok, test2.a is in a closure and still exists
}

D Version 1 hat eine begrenzte Verschlussunterstützung. Zum Beispiel funktioniert der obige Code nicht richtig, da sich die Variable a auf dem Stapel befindet und nach der Rückkehr von test () die Verwendung nicht mehr gültig ist (wenn Sie foo höchstwahrscheinlich über dg () aufrufen, wird a ‘zurückgegeben. zufällige ‘ganze Zahl). Dies kann gelöst werden, indem die Variable ‘a’ explizit auf dem Heap zugewiesen wird oder indem Strukturen oder Klassen verwendet werden, um alle erforderlichen geschlossenen Variablen zu speichern und einen Delegaten aus einer Methode zu erstellen, die denselben Code implementiert. Abschlüsse können an andere Funktionen übergeben werden, sofern sie nur verwendet werden, solange die referenzierten Werte noch gültig sind (z. B. Aufrufen einer anderen Funktion mit einem Abschluss als Rückrufparameter) und zum Schreiben von generischem Datenverarbeitungscode nützlich sind, daher diese Einschränkung In der Praxis ist dies oft kein Problem.

Diese Einschränkung wurde in D Version 2 behoben – die Variable ‘a’ wird automatisch auf dem Heap zugewiesen, da sie in der inneren Funktion verwendet wird, und ein Delegat dieser Funktion kann sich dem aktuellen Bereich entziehen (durch Zuweisung zu dg oder return). Alle anderen lokalen Variablen (oder Argumente), auf die nicht von Delegaten verwiesen wird oder die nur von Delegierten referenziert werden, die sich dem aktuellen Bereich nicht entziehen, verbleiben auf dem Stapel, der einfacher und schneller als die Heap-Zuweisung ist. Gleiches gilt für die Klassenmethoden von inner, die auf die Variablen einer Funktion verweisen.

Funktionsobjekte (C ++)[edit]

C ++ ermöglicht das Definieren von Funktionsobjekten durch Überladen operator(). Diese Objekte verhalten sich ähnlich wie Funktionen in einer funktionalen Programmiersprache. Sie können zur Laufzeit erstellt werden und den Status enthalten, erfassen jedoch nicht implizit lokale Variablen wie Schließungen. Ab der Revision von 2011 unterstützt die C ++ – Sprache auch Closures, eine Art Funktionsobjekt, das automatisch aus einem speziellen Sprachkonstrukt namens erstellt wird Lambda-Ausdruck. Ein C ++ – Abschluss kann seinen Kontext entweder durch Speichern von Kopien der Variablen, auf die zugegriffen wird, als Mitglieder des Abschlussobjekts oder durch Referenz erfassen. Im letzteren Fall, wenn das Abschlussobjekt den Bereich eines referenzierten Objekts verlässt und dessen aufruft operator() verursacht undefiniertes Verhalten, da C ++ – Schließungen die Lebensdauer ihres Kontexts nicht verlängern.

void foo(string myname) {
    int y;
    vector<string> n;
    // ...
    auto i = std::find_if(n.begin(), n.end(),
               // this is the lambda expression:
               [&](const string& s) { return s != myname && s.size() > y; }
             );
    // 'i' is now either 'n.end()' or points to the first string in 'n'
    // which is not equal to 'myname' and whose length is greater than 'y'
}

Inline-Agenten (Eiffel)[edit]

Eiffel enthält Inline-Agenten, die Schließungen definieren. Ein Inline-Agent ist ein Objekt, das eine Routine darstellt und durch Angabe des Codes der Routine inline definiert wird. Zum Beispiel in

ok_button.click_event.subscribe (
	agent (x, y: INTEGER) do
		map.country_at_coordinates (x, y).display
	end
)

das Argument zu subscribe ist ein Agent, der eine Prozedur mit zwei Argumenten darstellt; Die Prozedur findet das Land an den entsprechenden Koordinaten und zeigt es an. Der gesamte Agent hat den Ereignistyp “abonniert” click_event Für eine bestimmte Schaltfläche wird die Prozedur ausgeführt, wenn eine Instanz des Ereignistyps auf dieser Schaltfläche auftritt – da ein Benutzer auf die Schaltfläche geklickt hat -, wobei die Mauskoordinaten als Argumente für übergeben werden x und y.

Die Hauptbeschränkung von Eiffel-Agenten, die sie von Abschlüssen in anderen Sprachen unterscheidet, besteht darin, dass sie nicht auf lokale Variablen aus dem umschließenden Bereich verweisen können. Diese Entwurfsentscheidung hilft dabei, Mehrdeutigkeiten zu vermeiden, wenn über einen lokalen Variablenwert in einem Abschluss gesprochen wird. Sollte dies der neueste Wert der Variablen oder der Wert sein, der beim Erstellen des Agenten erfasst wird? Nur Current (ein Verweis auf das aktuelle Objekt, analog zu this In Java) können die Funktionen und Argumente des Agenten selbst über den Agentenkörper aufgerufen werden. Die Werte der äußeren lokalen Variablen können übergeben werden, indem dem Agenten zusätzliche geschlossene Operanden bereitgestellt werden.

C ++ Builder __closure reserviertes Wort[edit]

Embarcadero C ++ Builder stellt das Reservewort __closure bereit, um einen Zeiger auf eine Methode mit einer ähnlichen Syntax wie ein Funktionszeiger bereitzustellen.[20]

In Standard C könnte man a schreiben typedef für einen Zeiger auf einen Funktionstyp mit der folgenden Syntax:

typedef void (*TMyFunctionPointer)( void );

In ähnlicher Weise können Sie a deklarieren typedef für einen Zeiger auf eine Methode mit der folgenden Syntax:

typedef void (__closure *TMyMethodPointer)();

Siehe auch[edit]

  1. ^ Die Funktion kann als Referenz auf eine Funktion gespeichert werden, beispielsweise als Funktionszeiger.
  2. ^ Diese Namen beziehen sich am häufigsten auf Werte, veränderbare Variablen oder Funktionen, können aber auch andere Entitäten wie Konstanten, Typen, Klassen oder Beschriftungen sein.

Verweise[edit]

  1. ^ Sussman und Steele. “Schema: Ein Interpreter für den erweiterten Lambda-Kalkül”. “… eine Datenstruktur, die einen Lambda-Ausdruck enthält, und eine Umgebung, die verwendet werden soll, wenn dieser Lambda-Ausdruck auf Argumente angewendet wird.” ((Wikisource)
  2. ^ David A. Turner (2012). “Einige Geschichte funktionaler Programmiersprachen”. Trends in der funktionalen Programmierung ’12. Abschnitt 2, Anmerkung 8 enthält die Behauptung über M-Ausdrücke.
  3. ^ PJ Landin (1964), Die mechanische Bewertung von Ausdrücken
  4. ^ Joel Moses (Juni 1970), Die Funktion von FUNCTION in LISP oder warum das FUNARG-Problem als Umgebungsproblem bezeichnet werden sollte, hdl:1721,1 / 5854, AI Memo 199, Eine nützliche Metapher für den Unterschied zwischen FUNCTION und QUOTE in LISP besteht darin, QUOTE als poröse oder offene Abdeckung der Funktion zu betrachten, da freie Variablen in die aktuelle Umgebung gelangen. FUNCTION wirkt als geschlossene oder nicht poröse Abdeckung (daher der von Landin verwendete Begriff “Verschluss”). Daher sprechen wir von “offenen” Lambda-Ausdrücken (Funktionen in LISP sind normalerweise Lambda-Ausdrücke) und “geschlossenen” Lambda-Ausdrücken. […] Mein Interesse am Umweltproblem begann, als Landin, der ein tiefes Verständnis für das Problem hatte, zwischen 1966 und 1967 das MIT besuchte. Ich erkannte dann die Entsprechung zwischen den FUNARG-Listen, die das Ergebnis der Bewertung “geschlossener” Lambda-Ausdrücke in LISP und den Lambda-Verschlüssen von ISWIM sind.
  5. ^ Åke Wikström (1987). Funktionsprogrammierung mit Standard ML. ISBN 0-13-331968-7. Der Grund, warum es als “Abschluss” bezeichnet wird, ist, dass ein Ausdruck, der freie Variablen enthält, als “offener” Ausdruck bezeichnet wird. Wenn Sie ihm die Bindungen seiner freien Variablen zuordnen, schließen Sie ihn.
  6. ^ Gerald Jay Sussman und Guy L. Steele, Jr. (Dezember 1975), Schema: Ein Interpreter für den erweiterten Lambda-Kalkül, AI Memo 349
  7. ^ “array.filter”. Mozilla Developer Center. 10. Januar 2010. Abgerufen 9. Februar 2010.
  8. ^ “Re: FP, OO und Beziehungen. Trumpft jemand die anderen?”. 29. Dezember 1999. Archiviert von das Original am 26. Dezember 2008. Abgerufen 23. Dezember 2008.
  9. ^ Lambda-Ausdrücke und Verschlüsse C ++ Standards Committee. 29. Februar 2008.
  10. ^ GCC Handbuch, 6.4 Verschachtelte Funktionen, “Wenn Sie versuchen, die verschachtelte Funktion über ihre Adresse aufzurufen, nachdem die enthaltende Funktion beendet wurde, bricht die Hölle los. Wenn Sie versuchen, sie aufzurufen, nachdem eine enthaltende Bereichsebene beendet wurde, und wenn sie sich auf einige der Variablen bezieht, die nicht mehr vorhanden sind.” Im Bereich haben Sie vielleicht Glück, aber es ist nicht ratsam, das Risiko einzugehen. Wenn sich die verschachtelte Funktion jedoch nicht auf etwas bezieht, das außerhalb des Bereichs liegt, sollten Sie sicher sein. “
  11. ^ Grundlagen der Schauspielersemantik Will Clinger. MIT Mathematics Doktorarbeit. Juni 1981.
  12. ^ “Function.prototype.bind ()”. MDN-Webdokumente. Abgerufen 20. November 2018.
  13. ^ “Verschlüsse”. MDN-Webdokumente. Abgerufen 20. November 2018.
  14. ^ “Lambda-Ausdrücke (Die Java-Tutorials)”.
  15. ^ “Verschachtelte, innere, Mitglieder- und Top-Level-Klassen”.
  16. ^ “Beispiel für eine innere Klasse (Die Java-Tutorials> Lernen der Java-Sprache> Klassen und Objekte)”.
  17. ^ “Verschachtelte Klassen (Die Java-Tutorials> Lernen der Java-Sprache> Klassen und Objekte)”.
  18. ^ Apple Inc. “Blockiert Programmierthemen”. Abgerufen 8. März 2011.
  19. ^ Joachim Bengtsson (7. Juli 2010). “Programmieren mit C-Blöcken auf Apple-Geräten”. Archiviert von das Original am 25. Oktober 2010. Abgerufen 18. September 2010.
  20. ^ Die vollständige Dokumentation finden Sie unter http://docwiki.embarcadero.com/RADStudio/Rio/en/Closure

Externe Links[edit]

after-content-x4