Streicheln lassen lassen Haskell

8

Ich sollte wahrscheinlich zuerst erwähnen, dass ich Haskell ziemlich neu bin. Gibt es einen besonderen Grund, den Ausdruck let in Haskell beizubehalten?

Ich weiß, dass Haskell das Schlüsselwort rec losgeworden ist, das dem Y-Kombinatorteil einer let -Anweisung entspricht, die anzeigt, dass es rekursiv ist. Warum haben sie die let -Anweisung überhaupt nicht losgeworden?

Wenn sie es taten, werden Aussagen bis zu einem gewissen Grad iterativer. Zum Beispiel etwas wie:

%Vor%

wäre nur:

%Vor%

Was für Neulinge in der funktionalen Programmierung besser lesbar und einfacher ist. Der einzige Grund, warum ich daran denke, es zu behalten, ist ungefähr so:

%Vor%

Was würde das ohne die let aussehen, was meiner Meinung nach mehrdeutige Grammatik ist:

%Vor%

Aber wenn Haskell Whitespace nicht ignorierte und Codeblöcke / Scope ähnlich wie Python funktionierten, konnte es die let entfernen?

Gibt es einen stärkeren Grund, um let zu behalten?

Tut mir leid, wenn diese Frage dumm erscheint, ich versuche nur mehr darüber zu verstehen, warum es da drin ist.

    
antimatter 04.05.2014, 03:39
quelle

6 Antworten

9

Syntaktisch können Sie sich eine Sprache ohne let leicht vorstellen. Sofort können wir dies in Haskell produzieren, indem wir uns einfach auf where verlassen, wenn wir das wollten. Darüber hinaus gibt es viele mögliche Syntaxen.

Semantisch könnte man denken, dass wir uns auf etwas wie diese übertragen könnten

%Vor%

und tatsächlich sind diese beiden Ausdrücke zur Laufzeit identisch (modulo-rekursive Bindungen, die aber mit fix erreicht werden können). Traditionell weist let jedoch eine spezielle Typisierungssemantik auf (zusammen mit where und Top-Level-Namensdefinitionen ... die alle effektiv Syntaxzucker für let sind).

Insbesondere im Hindley-Milner-System, das die Grundlage von Haskell bildet, gibt es eine Vorstellung von let -generalisation. Intuitiv betrachtet es Situationen, in denen wir Funktionen auf ihre polymorphste Form aufrüsten. Insbesondere, wenn eine Funktion in einem Ausdruck irgendwo mit einem Typ wie

erscheint %Vor%

Diese Variablen, a , b und c , haben möglicherweise bereits eine Bedeutung in diesem Ausdruck. Insbesondere wird davon ausgegangen, dass es sich um feste, noch unbekannte Typen handelt. Vergleichen Sie das mit dem Typ

%Vor%

was den Begriff Polymorphismus beinhaltet, indem sofort gesagt wird, dass selbst dann, wenn die Typenvariablen a , b und c in der Umgebung verfügbar sind, Diese Referenzen sind frisch .

Dies ist ein unglaublich wichtiger Schritt im HM-Inferenzalgorithmus, da Polymorphie erzeugt wird und es HM ermöglicht, seine allgemeineren Typen zu erreichen. Leider ist es nicht möglich, diesen Schritt zu machen, wann immer es uns gefällt - es muss an kontrollierten Punkten erfolgen.

Dies ist es, was let -generalisation bewirkt: Es besagt, dass Typen für polymorphe Typen verallgemeinert werden sollten, wenn sie let -gebunden an einen bestimmten Namen sind. Eine solche Verallgemeinerung tritt nicht auf, wenn sie lediglich in Funktionen als Argumente übergeben werden.

Letztendlich benötigen Sie eine Form von "let", um den HM-Inferenzalgorithmus auszuführen. Außerdem kann es nicht nur für die Funktionsanwendung Syntaxzucker sein, obwohl sie äquivalente Laufzeiteigenschaften haben.

Syntaktisch könnte diese "let" -Konzeption let oder where oder eine Konvention der Namensbindung auf höchster Ebene heißen (alle drei sind in Haskell verfügbar). Solange es existiert und eine primäre Methode ist, um gebundene Namen zu erzeugen, wo Menschen Polymorphismus erwarten, wird es das richtige Verhalten haben.

    
J. Abrahamson 04.05.2014, 05:06
quelle
8

Es gibt wichtige Gründe, warum Haskell und andere funktionale Sprachen let verwenden. Ich werde versuchen, sie Schritt für Schritt zu beschreiben:

Quantifizierung von Typvariablen

Das Damas-Hindley-Milner-Typsystem , das in Haskell und anderen funktionalen Sprachen verwendet wird, erlaubt polymorphe Typen, aber die Typ-Quantoren sind nur vor eines bestimmten Ausdrucks erlaubt. Zum Beispiel, wenn wir

schreiben %Vor%

dann ist der Typ von const polymorph, implizit wird er allgemein als

quantifiziert %Vor%

und const können auf jeden Typ spezialisiert werden, den wir erhalten, indem wir zwei Typenausdrücke für a und b ersetzen.

Allerdings erlaubt das Typsystem keine Quantifizierer in Typausdrücken, wie

%Vor%

Solche Typen sind in System F erlaubt, aber dann tippt man ein und schreibt die Inferenz ist unentscheidbar, was bedeutet, dass der Compiler wäre nicht in der Lage, Typen für uns abzuleiten, und wir müssten Ausdrücke ausdrücklich mit Typen versehen.

(Lange Zeit war die Frage der Entscheidbarkeit der Typüberprüfung in System F offen, und es wurde manchmal als "peinliches offenes Problem" angesprochen, weil die Unentscheidbarkeit für viele andere Systeme nachgewiesen wurde, aber dieses eine, bis 1994 von Joe Wells bewiesen.)

(Mit GHC können Sie solche expliziten inneren Quantifizierer mit dem RankNTypes Erweiterung, aber wie bereits erwähnt, können die Typen nicht automatisch abgeleitet werden."

Arten von Lambda-Abstraktionen

Betrachten Sie den Ausdruck λx.M oder in der Haskell-Notation \x -> M , Dabei ist M ein Begriff, der x enthält. Wenn der Typ von x ist a und der Typ von M ist b , dann ist der Typ des gesamten Ausdrucks λx.M : a → b . Wegen der obigen Einschränkung darf a nicht ∀ enthalten, daher kann der Typ von x keine Typ-Quantoren enthalten, er kann nicht polymorph sein (oder anders ausgedrückt monomorph ).

Warum Lambda-Abstraktion nicht genug ist

Betrachten Sie dieses einfache Haskell-Programm:

%Vor%

Lassen Sie uns jetzt ignorieren, dass foo nicht sehr nützlich ist. Der Hauptpunkt ist, dass id in der Definition von foo mit zwei verschiedenen Typen instanziiert wird. Der erste

%Vor%

und der zweite

%Vor%

Wenn wir nun versuchen, dieses Programm in die reine Lambda-Kalkül-Syntax ohne let zu konvertieren, würden wir mit

enden %Vor%

wobei der erste Teil die Definition von foo und der zweite Teil die Definition von i ist. Aber dieser Begriff wird keine Überprüfung geben. Das Problem ist, dass i einen monomorphen Typ haben muss (wie oben beschrieben), aber wir müssen es polymorph machen, damit i auf die zwei verschiedenen Typen instanziiert werden kann.

Wenn Sie in Haskell versuchen, i -> i i zu schreiben, wird dies fehlschlagen. Es gibt keinen monomorphen Typ, den wir i zuweisen können, sodass i i typecheck.

würde

let löst das Problem

Wenn wir let i x = x in i i schreiben, ist die Situation anders. Anders als im vorherigen Absatz gibt es hier kein Lambda, es gibt keinen in sich geschlossenen Ausdruck wie λi.i i , wo wir einen polymorphen Typ für die abstrahierte Variable i benötigen würden. Daher kann let zulassen, dass i einen polymorphen Typ hat, in diesem Fall ∀a.a → a und so i i typechecks.

Ohne let , wenn wir ein Haskell-Programm kompiliert und in einen einzelnen Lambda-Term konvertiert hätten, müsste jeder Funktion ein einzelner monomorpher Typ zugewiesen werden! Das wäre ziemlich nutzlos.

So let ist eine essentielle Konstruktion, die Polymorhismus in Sprachen ermöglicht, die auf Damas-Hindley-Milner-Systemen basieren.

    
Petr Pudlák 04.05.2014 19:50
quelle
6

Die Geschichte von Haskell spricht ein wenig zu der Tatsache, dass Haskell seit langem eine komplexe Oberflächensyntax angenommen hat.

  

Es hat eine Weile gedauert, bis wir die stilistische Entscheidung getroffen haben, aber sobald wir dies getan haben, haben wir eine wütende Debatte darüber geführt, welcher Stil "besser" ist. Eine zugrunde liegende Annahme war, dass es möglichst "nur" geben sollte ein Weg, etwas zu tun, "so dass zum Beispiel sowohl lassen als auch wo überflüssig und verwirrend wäre.

     Am Ende haben wir die zugrundeliegende Annahme aufgegeben und die volle syntaktische Unterstützung für beide Stile zur Verfügung gestellt. Dies mag wie eine klassische Komitee-Entscheidung erscheinen, aber diese Meinung halten die jetzigen Autoren für eine gute Wahl, die wir jetzt als eine Stärke der Sprache betrachten. Unterschiedliche Konstrukte haben unterschiedliche Nuancen, und echte Programmierer verwenden in der Praxis sowohl let und wo, sowohl Wächter als auch Bedingungen, sowohl pattern-matching-Definitionen als auch case-Ausdrücke - nicht nur im selben Programm, sondern manchmal in der gleichen Funktionsdefinition. Es ist sicherlich richtig, dass der zusätzliche syntaktische Zucker die Sprache komplizierter erscheinen lässt, aber es ist eine oberflächliche Art von Komplexität, leicht erklärt durch rein syntaktische Transformationen.

    
Edward KMETT 04.05.2014 07:53
quelle
4

Das ist keine dumme Frage. Es ist völlig in Ordnung.

Erstens sind let / in Bindings syntaktisch eindeutig und können auf einfache mechanische Weise in lambdas umgeschrieben werden.

Zweitens, und deshalb ist let ... in ... ein Ausdruck: das heißt, er kann geschrieben werden, wo Ausdrücke erlaubt sind. Im Gegensatz dazu ist Ihre vorgeschlagene Syntax ähnlicher mit where , die an ein umgebendes syntaktisches Konstrukt gebunden ist, wie die Mustervergleichszeile einer Funktionsdefinition.

Man könnte auch argumentieren, dass Ihre vorgeschlagene Syntax im Stil zu imperativ ist, aber das ist sicherlich subjektiv.

Vielleicht bevorzugen Sie where bis let . Viele Haskell-Entwickler tun es. Es ist eine vernünftige Wahl.

    
Rein Henrichs 04.05.2014 04:11
quelle
3

Es gibt einen guten Grund, warum let da ist:

  • let kann in der do -Notation verwendet werden.
  • Es kann innerhalb des Listenverständnisses verwendet werden.
  • Es kann innerhalb der Funktionsdefinition wie erwähnt hier verwendet werden.

Sie geben das folgende Beispiel als eine Alternative zu let :

%Vor%

Das obige Beispiel wird nicht typencheck und die y und z werden auch zur Verschmutzung des globalen Namespace führen, was mit let vermieden werden kann.

    
Sibi 04.05.2014 03:56
quelle
3

Teil des Grundes, warum Haskells let so aussieht, als ob es funktioniert, ist auch die konsistente Art und Weise, wie es seine Eindruckempfindlichkeit verwaltet. Jedes einrucksempfindliche Konstrukt funktioniert auf die gleiche Weise: Zuerst gibt es ein einleitendes Schlüsselwort ( let , where , do , of ); dann bestimmt die Position des nächsten Tokens, was die Einrückungsstufe für diesen Block ist; und nachfolgende Zeilen, die auf derselben Ebene beginnen, werden als neues Element im Block angesehen. Deshalb können Sie

haben %Vor%

oder

%Vor%

aber nicht

%Vor%

Ich denke, es könnte tatsächlich möglich sein, schlüsselwortlose indentationsbasierte Bindungen zu verwenden, ohne die Syntax technisch mehrdeutig zu machen. Aber ich denke, dass es in der gegenwärtigen Konsistenz einen Wert gibt, zumindest für das Prinzip der geringsten Überraschung. Sobald Sie sehen, wie ein indentationsempfindliches Konstrukt funktioniert, funktionieren alle gleich. Und als Bonus haben sie alle das gleiche indentationsunabhängige Äquivalent. Das

%Vor%

entspricht immer

%Vor%

Als Entwickler von F # ist das etwas, was ich von Haskell beneide: Die Einrückungsregeln von F # sind komplexer und nicht immer konsistent.

    
Tarmil 04.05.2014 09:39
quelle

Tags und Links