971 Stimmen

Was ist der Unterschied zwischen einer "Closure" und einem "Lambda"?

Kann mir das jemand erklären? Ich verstehe die grundlegenden Konzepte dahinter, aber ich sehe oft, dass sie austauschbar verwendet werden, und das verwirrt mich.

Und wenn wir schon dabei sind, wie unterscheiden sie sich von einer normalen Funktion?

5voto

j2emanue Punkte 56131

Ein Lambda-Ausdruck ist einfach eine anonyme Funktion. In einfachem Java können Sie ihn zum Beispiel so schreiben:

Function<Person, Job> mapPersonToJob = new Function<Person, Job>() {
    public Job apply(Person person) {
        Job job = new Job(person.getPersonId(), person.getJobDescription());
        return job;
    }
};

wo die Klasse Function einfach in Java-Code eingebaut ist. Jetzt können Sie aufrufen mapPersonToJob.apply(person) irgendwo zu verwenden. Das ist nur ein Beispiel. Das ist ein Lambda, bevor es eine Syntax dafür gab. Lambdas sind eine Abkürzung dafür.

Schließung:

ein Lambda wird eine Schließung, wenn es auf die Variablen außerhalb dieses Bereichs zugreifen kann. ich schätze, Sie können sagen, seine Magie, es kann magisch um die Umgebung wickeln, die es erstellt wurde, und verwenden Sie die Variablen außerhalb seines Bereichs (outer scope. so um klar zu sein, eine Schließung bedeutet ein Lambda kann seine OUTER SCOPE zugreifen.

in Kotlin kann ein Lambda immer auf seine Closure (die Variablen, die sich in seinem äußeren Bereich befinden) zugreifen

4voto

FrankHB Punkte 1919

Es gibt viele Geräusche von technisch vagen oder "nicht einmal falschen" Kunstperlen in verschiedenen bestehenden Antworten auf diese Frage, also würde ich endlich eine neue hinzufügen...

Klärung der Terminologie

Es ist besser zu wissen, dass die Begriffe "Closure" und "Lambda" beide verschiedene Dinge bezeichnen können, die vom Kontext abhängen.

Dies ist ein formales Problem, da die Spezifikation der diskutierten PL (Programmiersprache) solche Begriffe explizit definieren kann.

Zum Beispiel durch ISO C++ (seit C++11):

Der Typ eines Lambda-Ausdrucks (der auch der Typ des Closure-Objekts ist) ist ein eindeutiger, unbenannter Nicht-Union-Klassentyp, genannt Closure-Typ, dessen Eigenschaften im Folgenden beschrieben werden.

Da Benutzer von C-ähnlichen Sprachen täglich "Zeiger" (Typen) mit "Zeigerwerten" oder "Zeigerobjekten" (Bewohnern von Typen) verwechseln, besteht auch hier Verwechslungsgefahr: Die meisten C++-Benutzer sprechen eigentlich von "Closure-Objekten", wenn sie den Begriff "Closure" verwenden. Seien Sie vorsichtig mit der Zweideutigkeit.

NOTA Um die Dinge im Allgemeinen klarer und präziser zu machen, verwende ich selten absichtlich einige sprachneutrale Begriffe (normalerweise spezifisch für die PL-Theorie anstelle der in der Sprache definierten Terminologie. Zum Beispiel, Typ Einwohner umfasst die sprachspezifischen "(r)Werte" und "lWerte" in einem weiteren Sinne. (Da die syntaktische Essenz von C++'s Wertkategorie Definition ist irrelevant, die Vermeidung von "(l/r)-Werten" kann die Verwirrung verringern). (Haftungsausschluss: lvalues und rvalues sind gemeinsame genug in vielen anderen Zusammenhängen). Begriffe, die in den verschiedenen PLs nicht formell definiert sind, können in Anführungszeichen gesetzt werden. Wörtliche Abschriften aus referenzierten Materialien können ebenfalls in Anführungszeichen gesetzt werden, wobei Tippfehler unverändert bleiben.

Dies gilt umso mehr für "lambda". Der (klein geschriebene) Buchstabe lambda () ist ein Element des griechischen Alphabets. Wenn man ihn mit "Lambda" und "Closure" vergleicht, ist sicherlich nicht der Buchstabe selbst gemeint, sondern etwas hinter der Syntax, das von "Lambda" abgeleitete Konzepte verwendet.

Die entsprechenden Konstrukte in modernen PLs werden gewöhnlich als "Lambda-Ausdrücke" bezeichnet. Sie sind von den "Lambda-Abstraktionen" abgeleitet, die weiter unten besprochen werden.

Vor den ausführlichen Diskussionen empfehle ich, einige Kommentare zur Frage selbst zu lesen. Ich halte sie für sicherer und hilfreicher als die meisten Antworten auf die Frage hier, im Sinne von weniger Verwechslungsgefahr. (Leider ist dies der wichtigste Grund, warum ich mich entschlossen habe, hier eine Antwort zu geben...)

Lambdas: eine kurze Geschichte

Die Konstrukte mit dem Namen "Lambda" in PLs, egal ob "Lambda-Ausdruck" oder etwas anderes, sind syntaktische . Mit anderen Worten, die Benutzer der Sprachen können solche Konstruktionen der Ausgangssprache die verwendet werden, um etwas anderes zu bauen. Grob gesagt sind die "anderen" in der Praxis einfach "anonyme Funktionen".

Solche Konstrukte stammen aus Lambda-Abstraktionen eine der drei Syntaxkategorien ("Arten von Ausdrücken") des (untypisierter) Lambda-Kalkül entwickelt von A. Church.

Das Lambda-Kalkül ist ein Deduktionssystem (genauer gesagt, ein TRS (Term Rewrite System) ) zur universellen Modellierung von Berechnungen. Die Reduktion eines Lambda-Terms ist genauso wie die Auswertung eines Ausdrucks in normalen PLs. Mit den eingebauten Reduktionsregeln ist es ausreichend, die verschiedenen Arten der Berechnung zu definieren. (Wie Sie vielleicht wissen, es ist Turing-komplett .) Daher kann es als PL verwendet werden.

NOTA Die Auswertung eines Ausdrucks in einem PL ist nicht austauschbar mit der Reduzierung eines Terms in einem TRS im Allgemeinen. Das Lambda-Kalkül ist jedoch eine Sprache, in der alle Reduktionsergebnisse in der Ausgangssprache (d. h. als Lambda-Terme) ausgedrückt werden können, so dass sie zufällig dieselbe Bedeutung haben. Fast alle PLs in der Praxis haben diese Eigenschaft nicht; der Kalkül zur Beschreibung ihrer Semantik kann Terme enthalten, die nicht die Ausdrücke der Ausgangssprache sind, und Reduktionen können detailliertere Auswirkungen als Bewertungen haben.

Alle Begriffe ("Ausdrücke") im Lambda-Kalkül (Lambda-Terme) sind entweder variabel, Abstraktion oder Anwendung. "Variabel" ist hier die Syntax (nur der Variablenname) des Symbols, das sich auf eine bestehende "Variable" (semantisch eine Entität, die auf einen anderen Lambda-Term reduziert werden kann) beziehen kann, die zuvor eingeführt wurde. Die Möglichkeit, eine Variable einzuführen, wird durch die Abstraktionssyntax bereitgestellt, die einen führenden Buchstaben hat, gefolgt von einem gebundene Variable , ein Punkt und ein Lambda-Term. Die gebundene Variable ähnelt in vielen Sprachen sowohl in der Syntax als auch in der Semantik dem formalen Parameternamen, und der nachfolgende Lambda-Term innerhalb der Lambda-Abstraktion ist genau wie der Funktionskörper. Die Anwendungssyntax verbindet einen Lambda-Term ("eigentliches Argument") mit einer Abstraktion, wie der Funktionsaufruf-Ausdruck in vielen PLs.

NOTA Eine Lambda-Abstraktion kann nur einen Parameter einführen. Um diese Einschränkung innerhalb des Kalküls zu überwinden, siehe Currying .

Die Möglichkeit, Variablen einzuführen, macht das Lambda-Kalkül zu einer typischen Hochsprache (wenn auch einer einfachen). Andererseits, kombinatorische Logik können als PLs behandelt werden, indem die Variablen- und Abstraktionsmerkmale aus dem Lambda-Kalkül entfernt werden. Kombinatorische Logiken sind genau in diesem Sinne low-level: sie sind wie einfache alte Assemblersprachen, die es nicht erlauben, vom Benutzer benannte Variablen einzuführen (trotz Makros, was zusätzliche Vorverarbeitung erfordert). (... Wenn schon nicht Low-Level... so können Assemblersprachen zumindest benutzerdefinierte Bezeichnungen einführen.)

Die Lambda-Abstraktion kann an Ort und Stelle in jeden anderen Lambda-Term eingebaut werden, ohne dass ein Name zur Bezeichnung der Abstraktion angegeben werden muss. Die Lambda-Abstraktion als Ganzes bildet also die (wahrscheinlich verschachtelte) anonyme Funktion. Dies ist ein recht anspruchsvolles Merkmal (im Vergleich z. B. zu ISO C, das keine anonymen oder verschachtelten Funktionen zulässt).

Zu den Nachfolgern des untypisierten Lambda-Kalküls gehören verschiedene typisierte Lambda-Kalküle (wie die Lambda-Würfel ). Diese sind eher wie statisch typisierte Sprachen, die Typ-Annotationen zu den formalen Parametern von Funktionen erfordern. Nichtsdestotrotz haben die Lambda-Abstraktionen hier immer noch die gleichen Funktionen.

Obwohl Lambda-Kalküle nicht dazu gedacht sind, direkt als in Computern implementierte PLs verwendet zu werden, haben sie in der Praxis Auswirkungen auf PLs. Vor allem J. McCarthy führte das LAMBDA Operator in LISP, um Funktionen bereitzustellen, die genau der Idee des untypisierten Lambda-Kalküls von Church entsprechen. Offensichtlich ist der Name LAMBDA stammt aus dem Buchstaben . LISP (später) hat eine andere Syntax ( S-Ausdruck ), aber alle programmierbaren Elemente in der LAMBDA Ausdrücke können durch triviale syntaktische Umwandlungen direkt auf die Lambda-Abstraktionen im untypisierten Lambda-Kalkül abgebildet werden.

Andererseits drücken viele andere PLs ähnliche Funktionalitäten auf andere Weise aus. Eine etwas andere Art, wiederverwendbare Berechnungen einzuführen, sind benannte Funktionen (oder genauer gesagt, benannte Unterprogramme), die von früheren PLs wie FORTRAN und von ALOL abgeleiteten Sprachen unterstützt werden. Sie werden durch Syntaxen eingeführt, die eine benannte Entität spezifizieren, die gleichzeitig eine Funktion ist. Dies ist in gewissem Sinne einfacher als LISP-Dialekte (insbesondere im Hinblick auf die Implementierung) und scheint seit Jahrzehnten beliebter zu sein als LISP-Dialekte. Benannte Funktionen können auch Erweiterungen erlauben, die anonyme Funktionen nicht haben, wie z.B. das Überladen von Funktionen.

Dennoch entdecken immer mehr industrielle Programmierer die Nützlichkeit von erstklassige Funktionen und die Anforderungen an die Fähigkeit, Funktionsdefinitionen an Ort und Stelle einzuführen (in den Ausdrücken in beliebigen Kontexten, z. B. als Argument einer anderen Funktion), steigen. Es ist natürlich und legitim, eine Sache, die nicht sein muss, nicht zu benennen, und alle benannten Funktionen versagen hier per Definition. (Sie kennen das vielleicht, Die korrekte Benennung von Dingen ist eines der bekanntesten schwierigen Probleme in der Informatik .) Um das Problem zu lösen, werden anonyme Funktionen in Sprachen eingeführt, die traditionell nur benannte Funktionen (oder funktionsähnliche Konstrukte wie "Methoden") anbieten, wie C++ und Java. Viele von ihnen nennen die Funktion "Lambda-Ausdrücke" oder ähnliche Lambda-Dinge, weil sie im Grunde die gleiche Idee in Lambda-Kalkülen widerspiegeln. Renaissance.

Zur Klarstellung: Im Lambda-Kalkül sind alle Terme (Variablen, Abstraktionen und Anwendungen) effektiv Ausdrücke in einem PL; sie sind alle "Lambda-Ausdrücke" in diesem Sinne. PLs, die eine Lambda-Abstraktion hinzufügen, um ihre Funktionen zu erweitern, können jedoch die Syntax der Abstraktion ausdrücklich als "Lambda-Ausdruck" bezeichnen, um sie von anderen Arten von Ausdrücken zu unterscheiden.

Verschlüsse: die Geschichte

Verschlüsse in der Mathematik ist nicht dasselbe wie es in PLs .

In letzterem Zusammenhang wird der Begriff wurde 1964 von P. J. Landin geprägt um die Unterstützung der erstklassigen Funktionen bei der Durchführung der Auswertung der PLs "modelliert in Churchs -notation" zu gewährleisten.

Speziell für das von Landin vorgeschlagene Modell (die SECD-Maschine ), ein Abschluss besteht aus dem -Ausdruck und der Umgebung, in Bezug auf die er ausgewertet wurde oder genauer gesagt:

einen Umgebungsteil, bei dem es sich um eine Liste handelt, deren zwei Elemente (1) eine Umgebung (2) ein Bezeichner oder eine Liste von Bezeichnern sind

und einem Kontrollteil, der aus einer Liste besteht, deren einziges Element ein AE ist

NOTA AE wird abgekürzt für applikativer Ausdruck in der Zeitung. Dies ist die Syntax, die mehr oder weniger die gleiche Funktionalität der Anwendung im Lambda-Kalkül darstellt. Es gibt auch einige zusätzliche Details wie "applikativ" im Lambda-Kalkül (weil es rein funktional ist) nicht so interessant. Die SECD ist aufgrund dieser kleinen Unterschiede nicht mit dem ursprünglichen Lambda-Kalkül vereinbar. Zum Beispiel hält die SECD bei jeder einzelnen Lambda-Abstraktion an, unabhängig davon, ob der Teilterm ("Körper") eine normale Form hat, weil sie den Teilterm nicht reduzieren ("den Körper auswerten") kann, ohne dass die Abstraktion angewendet ("aufgerufen") wurde. Ein solches Verhalten entspricht jedoch eher den heutigen PLs als dem Lambda-Kalkül. Die SECD ist auch nicht die einzige abstrakte Maschine, die Lambda-Terme auswerten kann; obwohl die meisten anderen abstrakten Maschinen für den gleichen Zweck auch Umgebungen haben können. Im Gegensatz zum Lambda-Kalkül (das rein ist), können diese abstrakten Maschinen in gewissem Maße Mutation unterstützen.

In diesem speziellen Kontext ist ein Abschluss also eine interne Datenstruktur, um spezifische Auswertungen von PLs mit AEs zu implementieren.

Die Disziplin des Zugriffs auf die Variablen in Verschlüssen spiegelt lexikalisches Scoping erstmals in den frühen 1960er Jahren von der imperativen Sprache ALGOL 60 verwendet. ALGOL 60 unterstützt zwar verschachtelte Prozeduren und die Übergabe von Prozeduren an Parameter, nicht aber die Rückgabe von Prozeduren als Ergebnis. Bei Sprachen mit vollständiger Unterstützung von Funktionen erster Klasse, die von Funktionen zurückgegeben werden können, funktioniert die statische Kette in Implementierungen im Stil von ALGOL 60 nicht, da freie Variablen, die von der zurückgegebenen Funktion verwendet werden, möglicherweise nicht mehr auf dem Aufrufstapel vorhanden sind. Dies ist die nach oben funarg Problem . Closures lösen das Problem, indem sie die freie Variable in den Umgebungsteilen festhalten und deren Zuweisung auf dem Stack vermeiden.

Andererseits verwenden die frühen LISP-Implementierungen alle einen dynamischen Bereich. Dadurch sind die Variablenbindungen, auf die verwiesen wird, alle im globalen Speicher erreichbar, und das Verstecken von Namen (falls vorhanden) wird als Basis pro Variable implementiert: Sobald eine Variable mit einem bestehenden Namen erstellt wird, wird der alte Name durch eine LIFO-Struktur gesichert; mit anderen Worten, jeder Variablenname kann auf einen entsprechenden globalen Stapel zugreifen. Dies macht die funktionsspezifischen Umgebungen überflüssig, da keine freien Variablen jemals in der Funktion erfasst werden (sie sind bereits von den Stapeln "erfasst").

Obwohl LISP zunächst die Lambda-Notation imitiert, unterscheidet es sich hier stark vom Lambda-Kalkül. Der Lambda-Kalkül ist statisch skaliert . Das heißt, jede Variable bezeichnet die Instanz, die durch den nächstgelegenen gleichnamigen formalen Parameter einer Lambda-Abstraktion begrenzt wird, die die Variable vor ihrer Reduktion enthält. In der Semantik des Lambda-Kalküls ersetzt die Reduktion einer Anwendung den Term ("Argument") durch die gebundene Variable ("formaler Parameter") in der Abstraktion. Da alle Werte im Lambda-Kalkül als Lambda-Terme dargestellt werden können, kann dies durch direktes Umschreiben geschehen, indem in jedem Schritt der Reduktion bestimmte Teilterme ersetzt werden.

NOTA Umgebungen sind also nicht unbedingt erforderlich, um die Lambda-Terme zu reduzieren. Ein Kalkül, das den Lambda-Kalkül erweitert, kann jedoch die Umgebungen explizit in die Grammatik einführen, selbst wenn es nur reine Berechnungen (ohne Mutation) modelliert. Durch das explizite Hinzufügen von Umgebungen kann es spezielle Regeln für die Umgebungsbeschränkungen geben, um Umgebungsnormalisierungen zu erzwingen, was die Gleichungstheorie des Kalküls stärkt. (Siehe [Shu10] §9.1.)

LISP ist ganz anders, weil die zugrunde liegenden semantischen Regeln weder auf Lambda-Kalkül noch auf Term-Rewriting basieren. Daher benötigt LISP einen anderen Mechanismus zur Aufrechterhaltung der Scoping-Disziplin. Dieser Mechanismus basiert auf den Umgebungsdatenstrukturen, in denen die Zuordnungen von Variablen zu Werten (d.h. Variablenbindungen) gespeichert werden. In neuen Varianten von LISP kann es eine komplexere Struktur in einer Umgebung geben (z.B. erlaubt lexically scoped Lisp Mutationen), aber die einfachste Struktur ist konzeptionell äquivalent zu der in Landins Papier definierten Umgebung, die unten diskutiert wird.

LISP-Implementierungen unterstützen Funktionen erster Klasse schon sehr früh, aber mit reinem dynamischen Scoping gibt es kein wirkliches Funargs-Problem: Sie können einfach die Zuweisungen auf dem Stack vermeiden und einem globalen Eigentümer (dem GC, Garbage Collector) die Verwaltung der Ressourcen in den Umgebungen (und Aktivierungsdatensätzen) überlassen, die auf die Variablen verweisen. Closures werden dann nicht benötigt. Und genau das haben die frühen Implementierungen vor der Erfindung von Closures getan.

Tiefe Bindung das die statische (lexikalische) Bindung annähert, wurde um 1962 in LISP 1.5 eingeführt, und zwar über die FUNARG Gerät. Dadurch wurde das Problem schließlich unter dem Namen "Funarg-Problem" bekannt.

NOTA AIM-199 weist darauf hin, dass es hier im Wesentlichen um die Umwelt geht.

Schema ist die erste Lisp-Dialekt Unterstützung des lexikalischen Scopings standardmäßig (der dynamische Geltungsbereich kann simuliert werden durch make-parameter / parameterize Formen in modernen Versionen von Scheme). In einem späteren Jahrzehnt gab es einige Debatten, aber schließlich übernahmen die meisten Lisp-Dialekte die Idee, lexikalisches Scoping zu verwenden, wie es viele andere Sprachen tun. Seitdem ist Closure als Implementierungstechnik unter den PLs verschiedener Geschmacksrichtungen weiter verbreitet und beliebter.

Verschlüsse: die Entwicklung

Im Originalbeitrag von Landin wird eine Umgebung zunächst als mathematische Funktion definiert, die den Namen ("constant") auf das benannte Objekt ("primitive") abbildet. Dann wird die Umgebung als "eine Listenstruktur, die aus Name/Wert-Paaren besteht" definiert. Letzteres ist auch in frühen Lisp-Implementierungen implementiert als alist s (assoziative Listen), aber moderne Sprachimplementierungen halten sich nicht unbedingt an solche Details. Insbesondere können Umgebungen verlinkt um verschachtelte Abschlüsse zu unterstützen, was von abstrakten Maschinen wie der SECD wahrscheinlich nicht direkt unterstützt wird.

Neben der Umgebung wird die andere Komponente des "Umgebungsteils" in Landins Papier verwendet, um die Namen der gebundenen Variablen der Lambda-Abstraktionen (die formalen Parameter der Funktionen) zu speichern. Dies ist ebenfalls optional (und fehlt wahrscheinlich) bei modernen Implementierungen, bei denen die Namen der Parameter statisch wegoptimiert werden können (im Geiste durch die Alpha-Renaming-Regeln der Lambda-Kalküle gewährt), wenn keine Notwendigkeit besteht, die Quellinformationen zu reflektieren.

In ähnlicher Weise speichern moderne Implementierungen die syntaktischen Konstrukte (AEs oder Lambda-Terme) möglicherweise nicht direkt als Kontrollteil. Stattdessen können sie eine interne IR (Zwischendarstellung) oder die "kompilierte" Form verwenden (z. B. FASL, das von einigen Implementierungen von Lisp-Dialekten verwendet wird). Eine solche IR wird nicht einmal garantiert aus lambda Formen (z.B. kann sie aus dem Körper einiger benannter Funktionen stammen).

Außerdem kann der Umgebungsteil für die Lambda-Kalküle andere Informationen speichern, die nicht zur Auswertung dienen. Zum Beispiel, er kann einen zusätzlichen Bezeichner beibehalten, um eine zusätzliche Bindung zu schaffen, indem er die Umgebung am Aufrufort benennt . Damit können Sprachen implementiert werden, die auf Erweiterungen von Lambda-Kalkülen basieren.

Überarbeitung der PL-spezifischen Terminologie

Darüber hinaus definieren einige Sprachen in ihrer Spezifikation "Closure"-bezogene Begriffe, um Entitäten zu benennen, die durch Closures implementiert werden können. Dies ist bedauerlich, weil es zu vielen Missverständnissen führt, wie z.B. "eine Closure ist eine Funktion". Aber glücklicherweise scheinen die meisten Sprachen es zu vermeiden, sie direkt als syntaktisches Konstrukt in der Sprache zu benennen.

Dennoch ist dies immer noch besser als das Überladen etablierter allgemeiner Konzepte, die durch Sprachspezifikationen beliebig sind. Um nur einige zu nennen:

  • "Objekte" werden zu "Instanzen von Klassen" umgelenkt (in Java /CLR/"OOP"-Sprachen) anstelle von traditionell "typisierte Speicherung" (in C und C++ ) oder nur "Werte" (in vielen Lisps);

  • "Variablen" werden auf etwas umgelenkt, das traditionell "Objekte" genannt wird (in Golang ) sowie veränderliche Zustände (in vielen neuen Sprachen), so dass sie nicht mehr mit der Mathematik und reinen funktionalen Sprachen kompatibel ist;

  • "Polymorphismus" ist beschränkt auf Einschlusspolymorphismus (in C++/"OOP"-Sprachen) haben auch diese Sprachen andere Arten von Polymorphismus (parametrischer Polymorphismus und Ad-hoc-Polymorphismus).

Über die Verwaltung der Ressourcen

Obwohl die Komponenten in modernen Implementierungen weggelassen werden, sind die Definitionen in Landins Papier ziemlich flexibel. Sie schränkt nicht ein, wie die Komponenten wie die Umgebungen außerhalb des Kontexts der SECD-Maschine zu speichern sind.

In der Praxis werden verschiedene Strategien angewandt. Die gebräuchlichste und traditionellste Methode besteht darin, alle Ressourcen einem globalen Eigentümer zuzuordnen, der die nicht mehr genutzten Ressourcen einsammeln kann, d. h. die (globale) GC, die erstmals in der LISP verwendet wurde.

Andere Wege benötigen möglicherweise keinen globalen Eigentümer und haben beispielsweise eine bessere Lokalisierung der Abschlüsse:

  • In C++ können die Ressourcen von Entitäten, die in Closures erfasst werden, vom Benutzer explizit verwaltet werden, indem er angibt, wie jede Variable in der Erfassungsliste des Lambda-Ausdrucks erfasst werden soll (durch Wertkopie, durch Referenz oder sogar durch einen expliziten Initialisierer) und den genauen Typ jeder Variablen (Smart Pointer oder andere Typen). Dies kann unsicher sein, bringt aber bei richtiger Anwendung mehr Flexibilität.

  • In Rust werden Ressourcen mit verschiedenen Erfassungsmodi (durch unveränderliches Ausleihen, durch Ausleihen, durch Verschieben) erfasst, die wiederum (von der Implementierung) ausprobiert werden, und Benutzer können explizit angeben move . Dies ist konservativer als C++, aber in gewissem Sinne sicherer (da Ausleihen statisch geprüft werden, im Gegensatz zu ungeprüften By-Reference-Captures in C++).

Alle oben genannten Strategien können Closures unterstützen (C++ und Rust haben sprachspezifische Definitionen des Konzepts "Closure Type"). Die Disziplinen zur Verwaltung der von den Closures verwendeten Ressourcen haben nichts mit der Qualifikation der Closures zu tun.

Also, (obwohl hier nicht zu sehen,) die Behauptung der Notwendigkeit von Graphenverfolgung für Abschlüsse durch Thomas Lord an der LtU ist auch technisch nicht korrekt. Closures können das funarg-Problem lösen, weil sie es ermöglichen, ungültige Zugriffe auf den Aktivierungssatz (den Stack) zu verhindern, aber die Tatsache, dass sie nicht auf magische Weise alle Operationen auf den Ressourcen, die die Closure umfassen, durchsetzen wird gültig sein. Solche Mechanismen hängen von der externen Ausführungsumgebung ab. Es sollte klar sein, dass auch in traditionellen Implementierungen der implizite Eigentümer (GC) keine Komponente ist in und die Existenz des Eigentümers ist ein Implementierungsdetail der SECD-Maschine (also eines der "hochrangigen" Details für die Benutzer). Ob ein solches Detail die Graphenverfolgung unterstützt oder nicht, hat keine Auswirkungen auf die Qualifikation von Abschlüssen. Außerdem, AFAIK, die Sprachkonstrukte let kombiniert mit rec wird erstmals (wiederum von P. Landin) in ISWIM 1966 eingeführt die keine Auswirkungen auf die Durchsetzung der ursprünglichen Bedeutung der vor ihr erfundenen Verschlüsse haben konnte.

Die Beziehungen

Zusammenfassend kann ein Abschluss also (informell) wie folgt definiert werden:

(1) eine PL-implementierungsspezifische Datenstruktur, die einen Umgebungsteil und einen Steuerungsteil für eine funktionsähnliche Einheit umfasst, wobei:

(1.1) wird der Kontrollteil von einigen Konstrukten der Ausgangssprache abgeleitet, die das Auswertungskonstrukt der funktionsähnlichen Entität spezifizieren;

(1.2) Der Umgebungsteil besteht aus einer Umgebung und optional aus anderen implementierungsdefinierten Daten;

(1.3) wird die Umgebung in (1.2) durch die potenziell kontextabhängigen Ausgangssprachkonstrukte der funktionsähnlichen Entität bestimmt, die verwendet werden, um die erfassten freien Variablen zu halten, die im Evaluationskonstrukt der Ausgangssprachkonstrukte auftreten, die die funktionsähnliche Entität erzeugen.

(2) alternativ: der Oberbegriff für eine Implementierungstechnik zur Nutzung der in (1) als "Verschlüsse" bezeichneten Einheiten.

Lambda-Ausdrücke (Abstraktionen) sind einfach einer von die syntaktischen Konstrukte in der Ausgangssprache, um unbenannte funktionsähnliche Einheiten einzuführen (zu erstellen). Eine PL kann dies als einzige Möglichkeit zur Einführung einer funktionsähnlichen Entität anbieten.

Im Allgemeinen gibt es keine eindeutige Entsprechung zwischen Lambda-Ausdrücken im Quellprogramm und dem Vorhandensein der Schließungen bei der Ausführung des Programms. Da Implementierungsdetails keine Auswirkungen auf das beobachtbare Verhalten des Programms haben, ist es einer PL-Implementierung in der Regel erlaubt, die für Closures zugewiesenen Ressourcen zusammenzulegen, wenn dies möglich ist, oder die Erstellung von Closures ganz zu unterlassen, wenn dies für die Semantik des Programms keine Rolle spielt:

  • Die Implementierung kann die Menge der freien Variablen prüfen, die im Lambda-Ausdruck erfasst werden sollen, und wenn die Menge leer ist, kann sie die Einführung des Umgebungsteils vermeiden, so dass die funktionsähnliche Entität keine Schließung erfordert, die gepflegt werden muss. Eine solche Strategie ist normalerweise in den Regeln statischer Sprachen vorgeschrieben.

  • Andernfalls kann es vorkommen, dass die Implementierung eine Schließung für eine funktionsähnliche Entität erstellt, die sich durch die Auswertung des Lambda-Ausdrucks ergibt, unabhängig davon, ob es zu erfassende Variablen gibt oder nicht.

Lambda-Ausdrücke können zu einer funktionsähnlichen Einheit ausgewertet werden. Benutzer einiger PLs bezeichnen eine solche funktionsähnliche Entität als "Closure". "Anonyme Funktion" sollte in diesem Zusammenhang eine neutralere Bezeichnung für eine solche "Schließung" sein.

Anhang: Funktionen: die chaotische Geschichte

Das hat zwar nicht direkt mit dem Problem zu tun, aber es ist vielleicht auch erwähnenswert, dass "Funktionen" in verschiedenen Kontexten unterschiedliche Entitäten bezeichnen können.

Es ist bereits ein Chaos in der Mathematik .

Im Moment bin ich zu faul, sie im Kontext der PLs zusammenzufassen, aber als Warnung: Achten Sie auf den Kontext, um sicherzustellen, dass die verschiedenen Definitionen von "Funktion" in verschiedenen PLs Ihre Argumentation nicht vom Thema ablenken.

Was die Verwendung "anonymer Funktionen" im Allgemeinen betrifft (die in der Praxis auch von PLs verwendet werden), so glaube ich, dass sie keine größeren Verwirrungen und Missverständnisse zu diesem Thema hervorrufen wird.

Bei benannten Funktionen kann es etwas mehr Probleme geben. Funktionen können sowohl die Entität des Namens selbst (die "Symbole"), als auch die ausgewerteten Werte dieser Namen bezeichnen. In Anbetracht der Tatsache, dass die meisten PLs keinen unbewerteten Kontext haben, um eine Funktion von anderen Entitäten mit interessanter Bedeutung zu unterscheiden (z. B. sizeof(a_plain_cxx_function) in C++ einfach nur schlecht geformt), können Benutzer die Unterschiede der Fehlinterpretation zwischen unbewertetem Operanden und bewerteten Werten nicht erkennen. Das wird bei einigen Lisp-Dialekten problematisch, die QUOTE . Selbst erfahrene PL-Spezialisten können leicht etwas Wichtiges übersehen Dies ist auch der Grund, warum ich betone, syntaktische Konstrukte von anderen Entitäten zu unterscheiden.

3voto

DIPANSHU GOYAL Punkte 51

Es hängt davon ab, ob eine Funktion eine externe Variable verwendet oder nicht, um eine Operation durchzuführen.

Externe Variablen - Variablen, die außerhalb des Anwendungsbereichs einer Funktion definiert sind.

  • Lambda-Ausdrücke sind zustandslos weil sie von Parametern, internen Variablen oder Konstanten abhängt, um Operationen durchzuführen.

    Function<Integer,Integer> lambda = t -> {
        int n = 2
        return t * n 
    }
  • Schließungen Haltezustand weil sie externe Variablen (d.h. Variablen, die außerhalb des Funktionskörpers definiert sind) zusammen mit Parametern und Konstanten verwendet, um Operationen durchzuführen.

    int n = 2
    
    Function<Integer,Integer> closure = t -> {
        return t * n 
    }

Wenn Java eine Schließung erstellt, bleibt die Variable n bei der Funktion, damit sie bei der Übergabe an andere Funktionen oder bei der Verwendung an anderer Stelle referenziert werden kann.

3voto

mtmrv Punkte 33

Die Frage ist 12 Jahre alt und wir bekommen sie immer noch als ersten Link in Google für "closures vs lambda". Ich muss es also sagen, da es niemand explizit getan hat.

Ein Lambda-Ausdruck ist eine anonyme Funktion (Deklaration).

Und ein Abschluss, Zitat Scotts Programmiersprachen-Pragmatik wird erklärt als:

eine explizite Darstellung einer referenzierenden Umgebung (im Allgemeinen diejenige, in der das Unterprogramm ausgeführt würde, wenn es zum gegenwärtigen Zeitpunkt aufgerufen würde) zu erstellen und sie zusammen mit einer Referenz auf das Unterprogramm zu bündeln wird als Verschluss .

Das heißt, es ist genau so, wie wir es nennen das Bündel von "Funktion + Aufgabe des Kontextes".

0voto

ap-osd Punkte 2196

Lambda ist eine anonyme Funktion Definition die nicht (notwendigerweise) an einen Bezeichner gebunden ist.

"Anonyme Funktionen haben ihren Ursprung in der Arbeit von Alonzo Church und seiner Erfindung des Lambda-Kalküls, in dem alle Funktionen anonym sind". Wikipedia

Closure ist die Implementierung der Lambda-Funktion.

"Peter J. Landin definierte 1964 den Begriff "Closure" als einen Umgebungs- und einen Kontrollteil, wie er von seiner SECD-Maschine zur Auswertung von Ausdrücken verwendet wird. Wikipedia

Die allgemeine Erklärung von Lambda und Closure wird in den anderen Antworten behandelt.

Für C++-Kenner: Lambda-Ausdrücke wurden in C++11 eingeführt. Lambdas sind eine praktische Möglichkeit, anonyme Funktionen und Funktionsobjekte zu erstellen.

"Die Unterscheidung zwischen einem Lambda und der zugehörigen Schließung entspricht genau der Unterscheidung zwischen einer Klasse und einer Instanz der Klasse. Eine Klasse existiert nur im Quellcode; sie existiert nicht zur Laufzeit. Was zur Laufzeit existiert, sind Objekte des Klassentyps. Closures sind für Lambdas das, was Objekte für Klassen sind. Das sollte nicht überraschen, denn jeder Lambda-Ausdruck führt dazu, dass (während der Kompilierung) eine eindeutige Klasse erzeugt wird und (zur Laufzeit) auch ein Objekt dieses Klassentyps, eine Closure, entsteht." - Scott Myers

In C++ können wir die Feinheiten von Lambda und Closure untersuchen, da die zu erfassenden freien Variablen explizit angegeben werden müssen.

Im folgenden Beispiel hat der Lambda-Ausdruck keine freien Variablen, eine leere Fangliste ( [] ). Es handelt sich im Wesentlichen um eine gewöhnliche Funktion, für die kein Abschluss im engeren Sinne erforderlich ist. Sie kann also sogar als Funktionszeiger-Argument übergeben werden.

void register_func(void(*f)(int val))   // Works only with an EMPTY capture list
{
    int val = 3;
    f(val);
}

int main() 
{
    int env = 5;
    register_func( [](int val){ /* lambda body can access only val variable*/ } );
}

Sobald eine freie Variable aus der Umgebung in die Capture-Liste eingeführt wird ( [env] ), muss eine Schließung erzeugt werden.

    register_func( [env](int val){ /* lambda body can access val and env variables*/ } );

Da es sich nicht mehr um eine gewöhnliche Funktion, sondern um eine Schließung handelt, kommt es zu einem Kompilierungsfehler.
no suitable conversion function from "lambda []void (int val)->void" to "void (*)(int val)" exists

Der Fehler kann mit einem Funktionswrapper behoben werden std::function die jedes aufrufbare Ziel, einschließlich einer generierten Schließung, akzeptiert.

void register_func(std::function<void(int val)> f)

Ver Lambda und Schließung für eine ausführliche Erklärung mit einem C++-Beispiel.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X