Warum nicht abhängig getippt werden?

161

Ich habe mehrere Quellen gesehen, die die Meinung widerspiegeln, dass "Haskell allmählich zu einer Sprache mit abhängiger Typisierung wird". Die Implikation scheint zu sein, dass Haskell mit immer mehr Spracherweiterungen in diese allgemeine Richtung driftet, aber noch nicht da ist.

Grundsätzlich gibt es zwei Dinge, die ich gerne wissen würde. Die erste ist, ganz einfach, was „eine Abhängigkeit typisierte Sprache ist“ nicht wirklich bedeuten ? (Hoffentlich ohne zu technisch zu sein.)

Die zweite Frage ist ... was ist der Nachteil? Ich meine, die Leute wissen, dass wir in diese Richtung gehen, also muss es einen Vorteil geben. Und doch sind wir noch nicht da, also muss es einen Nachteil geben, der die Leute davon abhält, den ganzen Weg zu gehen. Ich habe den Eindruck, dass das Problem eine steile Zunahme der Komplexität ist. Aber da ich nicht wirklich verstehe, was abhängiges Tippen ist, weiß ich es nicht genau.

Was ich tun wissen , ist , dass jedes Mal , wenn ich über eine abhängig typisierte Programmiersprache zu lesen beginnen, wird der Text völlig unverständlich ist ... Vermutlich das ist das Problem. (?)

MathematicalOrchid
quelle
10
Einfach ausgedrückt können Sie Typen schreiben, die von Begriffen (Berechnungen) abhängen. Dies reicht aus, um Typen für jeden Aspekt Ihres Programms anzugeben, und bedeutet daher, dass das Typsystem die vollständige Programmspezifikation ausführen kann. Das Problem ist, dass die Typprüfung erheblich schwieriger ist (im Allgemeinen unmöglich), da die Typen von Berechnungen abhängen.
GManNickG
27
@GManNickG: Typprüfung ist durchaus möglich. Typ - Inferenz ist eine andere Sache, aber dann wieder GHC die verschiedenen Erweiterungen haben längst die Idee aufgegeben , dass es möglich sein sollte , alle Arten zu schließen.
CA McCann
7
Wenn ich das richtig verstehe, ist der Nachteil, dass es schwierig ist, abhängiges Tippen richtig zu machen (z. B. auf eine Weise, die sowohl brauchbar als auch begründet ist) , und wir wissen noch nicht, wie genau.
Comingstorm
1
@ CamcCann: Ja, mein Fehler.
GManNickG
4
Ich glaube nicht, dass jemand auf den einen großen pragmatischen Nachteil hingewiesen hat: Es ist wahnsinnig langweilig, Beweise dafür zu schreiben, dass Ihr gesamter Code korrekt ist. Da Sie keine Typinferenz durchführen können (entspricht einem Satz, der sich in einer "hella mächtigen" Logik beweist), müssen Sie Anmerkungen für Ihr Programm in Form von Beweisen schreiben. Dies wird offensichtlich nach einer Weile ärgerlich und schwierig, insbesondere für die ausgefeiltere monadische Magie, die die Leute normalerweise in Haskell tun. Das Nächste, was wir heutzutage kommen, sind Sprachen, die das meiste für uns tun oder uns eine gute Reihe von Grundelementen geben.
Kristopher Micinski

Antworten:

21

Die abhängige Typisierung ist eigentlich nur die Vereinheitlichung der Werte und Typstufen. Sie können also Werte für Typen parametrisieren (bereits möglich mit Typklassen und parametrischem Polymorphismus in Haskell) und Typen für Werte parametrisieren (streng genommen noch nicht möglich in Haskell) , obwohl DataKinds sehr nahe kommt).

Edit: Anscheinend habe ich mich von diesem Punkt an geirrt (siehe @ pigworkers Kommentar). Ich werde den Rest als Aufzeichnung der Mythen aufbewahren, die ich gefüttert habe. : P.


Das Problem bei der Umstellung auf vollständig abhängige Typisierung besteht, wie ich gehört habe, darin, dass die Phasenbeschränkung zwischen den Typ- und Wertebenen aufgehoben wird, die es Haskell ermöglicht, zu effizientem Maschinencode mit gelöschten Typen zu kompilieren. Nach unserem derzeitigen Stand der Technik muss eine abhängig getippte Sprache vorhanden sein irgendwann einen Dolmetscher durchlaufen (entweder sofort oder nach zu abhängig typisierte Bytecode oder ähnlichem erstellt werden).

Dies ist nicht unbedingt eine grundlegende Einschränkung, aber mir persönlich sind keine aktuellen Forschungsergebnisse bekannt, die in dieser Hinsicht vielversprechend aussehen, die es jedoch noch nicht in die GHC geschafft haben. Wenn jemand mehr weiß, würde ich mich freuen, korrigiert zu werden.

Pthariens Flamme
quelle
46
Was Sie sagen, ist fast völlig falsch. Ich beschuldige Sie nicht ganz: Es wiederholt Standardmythen als Tatsache. Die Sprache von Edwin Brady, Idris, führt eine Typlöschung durch (da kein Laufzeitverhalten von den Typen abhängt) und generiert eine ziemlich standardmäßige Lambda-Lifted-Supercombinator-Codierung, aus der Code unter Verwendung von Standard-G-Maschinentechniken generiert wird.
Schweinearbeiter
3
Als Randnotiz hat mich kürzlich jemand auf dieses Papier hingewiesen . Nach allem, was ich sagen kann, würde es Haskell abhängig machen (dh die Sprache auf Typebene würde abhängig geschrieben werden), was so nah ist, wie ich sehen kann, dass wir es bald bekommen.
Ptharien Flamme
8
Ja, dieses Papier zeigt hauptsächlich, wie man Typen von Sachen auf Typebene abhängig macht (und die Unterscheidung zwischen Typ und Art beseitigt). Ein plausibles Follow-up, das bereits diskutiert wird, besteht darin, tatsächlich abhängige Funktionstypen zuzulassen, ihre Argumente jedoch auf das Fragment der Sprache zu beschränken, das sowohl in Wert- als auch in Typschichten vorhanden sein kann (jetzt dank Datentyp-Promotion nicht mehr trivial). Dies würde die Notwendigkeit der Singleton-Konstruktion beseitigen, was das "Fälschen" derzeit komplexer als wünschenswert macht. Wir nähern uns immer mehr der Realität.
Schweinearbeiter
13
Es gibt viele pragmatische Fragen, die abhängige Typen an Haskell nachrüsten. Sobald wir diese eingeschränkte Form des abhängigen Funktionsraums haben, stehen wir immer noch vor der Frage, wie das Fragment der auf Sprachebene zulässigen Wertesprache vergrößert werden kann und wie die Gleichungstheorie lauten soll (wie wir es von 2 + 2 erwarten sei 4 und so). Es gibt viele fummelige Probleme (z. B. unten), die von Grund auf neu getippte Sprachen von Anfang an entwerfen.
Schweinearbeiter
2
@pigworker Gibt es eine saubere Teilmenge von Haskell, die total ist? Wenn ja, könnten wir das nicht einfach für das "Fragment der Sprache verwenden, das sowohl in Wert- als auch in Textebenen existieren kann"? Wenn nicht, was würde es brauchen, um eine zu produzieren?
Pthariens Flamme
223

Abhängig getippt Haskell, jetzt?

Haskell ist in geringem Maße eine abhängig typisierte Sprache. Es gibt eine Vorstellung von Daten auf Typebene, die jetzt dank sinnvoller typisiert sind DataKinds, und es gibt einige Mittel ( GADTs), um Daten auf Typebene eine Laufzeitdarstellung zu geben. Daher werden Werte von Laufzeitmaterial effektiv in Typen angezeigt , was bedeutet, dass eine Sprache abhängig typisiert werden muss.

Einfache Datentypen werden unterstützt , um die Art Ebene, so dass die Werte , die sie enthalten , können in Typen verwendet werden. Daher das archetypische Beispiel

data Nat = Z | S Nat

data Vec :: Nat -> * -> * where
  VNil   :: Vec Z x
  VCons  :: x -> Vec n x -> Vec (S n) x

wird möglich, und damit Definitionen wie

vApply :: Vec n (s -> t) -> Vec n s -> Vec n t
vApply VNil         VNil         = VNil
vApply (VCons f fs) (VCons s ss) = VCons (f s) (vApply fs ss)

was nett ist. Beachten Sie, dass die Länge nin dieser Funktion rein statisch ist und sicherstellt, dass die Eingabe- und Ausgabevektoren dieselbe Länge haben, obwohl diese Länge bei der Ausführung von keine Rolle spielt vApply. Im Gegensatz dazu ist es viel schwieriger , (dh unmöglich) , um die Funktion zu implementieren , die machen nKopien eines bestimmten x(das wäre purezu vApply‚s <*>)

vReplicate :: x -> Vec n x

weil es wichtig ist zu wissen, wie viele Kopien zur Laufzeit erstellt werden sollen. Geben Sie Singletons ein.

data Natty :: Nat -> * where
  Zy :: Natty Z
  Sy :: Natty n -> Natty (S n)

Für jeden heraufstufbaren Typ können wir die Singleton-Familie erstellen, die über den heraufgestuften Typ indiziert ist und von Laufzeitduplikaten seiner Werte bewohnt wird. Natty nist der Typ der Laufzeitkopien der Textebene n :: Nat. Wir können jetzt schreiben

vReplicate :: Natty n -> x -> Vec n x
vReplicate Zy     x = VNil
vReplicate (Sy n) x = VCons x (vReplicate n x)

Dort haben Sie also einen Wert auf Typebene, der an einen Laufzeitwert gebunden ist: Durch Überprüfen der Laufzeitkopie wird das statische Wissen über den Wert auf Typebene verfeinert. Obwohl Begriffe und Typen getrennt sind, können wir abhängig arbeiten, indem wir die Singleton-Konstruktion als eine Art Epoxidharz verwenden und Bindungen zwischen den Phasen herstellen. Das ist weit davon entfernt, beliebige Laufzeitausdrücke in Typen zuzulassen, aber es ist nichts.

Was ist böse? Was fehlt?

Lassen Sie uns etwas Druck auf diese Technologie ausüben und sehen, was anfängt zu wackeln. Wir könnten auf die Idee kommen, dass Singletons etwas impliziter handhabbar sein sollten

class Nattily (n :: Nat) where
  natty :: Natty n
instance Nattily Z where
  natty = Zy
instance Nattily n => Nattily (S n) where
  natty = Sy natty

Erlaubt uns zu schreiben, sagen wir

instance Nattily n => Applicative (Vec n) where
  pure = vReplicate natty
  (<*>) = vApply

Das funktioniert, aber es bedeutet jetzt, dass unser Originaltyp Natdrei Kopien hervorgebracht hat: eine Art, eine Singleton-Familie und eine Singleton-Klasse. Wir haben einen ziemlich umständlichen Prozess für den Austausch expliziter Natty nWerte und Nattily nWörterbücher. Darüber hinaus Nattyist nicht Nat: Wir haben eine Art Abhängigkeit von Laufzeitwerten, aber nicht von dem Typ, an den wir zuerst gedacht haben. Keine vollständig abhängig getippte Sprache macht abhängige Typen so kompliziert!

In der Zwischenzeit Natkann zwar gefördert werden, Veckann aber nicht. Sie können nicht nach einem indizierten Typ indizieren. Voll von abhängig typisierten Sprachen, die keine solche Einschränkung auferlegen, und in meiner Karriere als abhängig typisierter Show-Off habe ich gelernt, Beispiele für die zweischichtige Indizierung in meine Vorträge aufzunehmen, nur um Leute zu unterrichten, die eine einschichtige Indizierung durchgeführt haben Es ist schwierig, aber möglich, nicht zu erwarten, dass ich mich wie ein Kartenhaus zusammenfalte. Was ist das Problem? Gleichberechtigung. GADTs übersetzen die Einschränkungen, die Sie implizit erreichen, wenn Sie einem Konstruktor einen bestimmten Rückgabetyp geben, in explizite Gleichungsanforderungen. So was.

data Vec (n :: Nat) (x :: *)
  = n ~ Z => VNil
  | forall m. n ~ S m => VCons x (Vec m x)

In jeder unserer beiden Gleichungen haben beide Seiten Art Nat.

Versuchen Sie nun dieselbe Übersetzung für etwas, das über Vektoren indiziert ist.

data InVec :: x -> Vec n x -> * where
  Here :: InVec z (VCons z zs)
  After :: InVec z ys -> InVec z (VCons y ys)

wird

data InVec (a :: x) (as :: Vec n x)
  = forall m z (zs :: Vec x m). (n ~ S m, as ~ VCons z zs) => Here
  | forall m y z (ys :: Vec x m). (n ~ S m, as ~ VCons y ys) => After (InVec z ys)

und jetzt bilden wir Gleichungsbeschränkungen zwischen as :: Vec n xund VCons z zs :: Vec (S m) xwo die beiden Seiten syntaktisch unterschiedliche (aber nachweislich gleiche) Arten haben. Der GHC-Kern ist derzeit nicht für ein solches Konzept ausgestattet!

Was fehlt noch? Nun, der größte Teil von Haskell fehlt auf der Typebene. Die Sprache der Begriffe, die Sie fördern können, enthält eigentlich nur Variablen und Nicht-GADT-Konstruktoren. Sobald Sie diese haben, type familykönnen Sie mit der Maschinerie Programme auf Textebene schreiben: Einige davon ähneln möglicherweise Funktionen, die Sie auf der Begriffsebene schreiben möchten (z. B. Ausstattung Natmit Addition, sodass Sie einen guten Typ zum Anhängen angeben können Vec). , aber das ist nur ein Zufall!

In der Praxis fehlt auch eine Bibliothek, die unsere neuen Fähigkeiten nutzt, um Typen nach Werten zu indizieren. Was tun Functor und Monadwerden in dieser schönen neuen Welt? Ich denke darüber nach, aber es gibt noch viel zu tun.

Ausführen von Programmen auf Typebene

Haskell hat, wie die meisten abhängig typisierten Programmiersprachen, zwei operative Semantiken. Es gibt die Art und Weise, wie das Laufzeitsystem Programme ausführt (nur geschlossene Ausdrücke, nach dem Löschen des Typs, stark optimiert), und es gibt die Art und Weise, wie der Typechecker Programme ausführt (Ihre Typfamilien, Ihre "Typklasse Prolog" mit offenen Ausdrücken). Bei Haskell verwechseln Sie die beiden normalerweise nicht, da die ausgeführten Programme in verschiedenen Sprachen ausgeführt werden. Abhängig typisierte Sprachen haben separate Laufzeit- und statische Ausführungsmodelle für dieselbe Programmsprache, aber keine Sorge, mit dem Laufzeitmodell können Sie weiterhin Löschvorgänge und Korrekturlöschvorgänge durchführen: Das ist Coqs Extraktion vonMechanismus gibt Ihnen; Das ist zumindest der Compiler von Edwin Brady (obwohl Edwin unnötig duplizierte Werte sowie Typen und Beweise löscht). Die Phasenunterscheidung ist möglicherweise keine Unterscheidung der syntaktischen Kategorie mehr, aber sie ist lebendig und gut.

Abhängig getippte Sprachen ermöglichen es dem Typechecker, Programme auszuführen, ohne Angst vor etwas Schlimmerem als langem Warten. Wenn Haskell abhängiger typisiert wird, stehen wir vor der Frage, wie sein statisches Ausführungsmodell aussehen soll. Ein Ansatz könnte darin bestehen, die statische Ausführung auf Gesamtfunktionen zu beschränken, was uns die gleiche Ausführungsfreiheit ermöglicht, uns jedoch dazu zwingen könnte, (zumindest für Code auf Typebene) zwischen Daten und Codaten zu unterscheiden, damit wir feststellen können, ob dies der Fall ist Kündigung oder Produktivität erzwingen. Dies ist jedoch nicht der einzige Ansatz. Es steht uns frei, ein viel schwächeres Ausführungsmodell zu wählen, das nur ungern Programme ausführt, auf Kosten weniger Gleichungen, die nur durch Berechnung herauskommen. Und genau das macht GHC tatsächlich. Die Typisierungsregeln für den GHC-Kern werden nicht erwähnt Ausführen Programme, aber nur zur Überprüfung von Beweisen für Gleichungen. Bei der Übersetzung in den Kern versucht der Einschränkungslöser von GHC, Ihre Programme auf Typebene auszuführen, und generiert eine kleine silberne Spur von Beweisen dafür, dass ein bestimmter Ausdruck seiner normalen Form entspricht. Diese Methode zur Erzeugung von Beweisen ist etwas unvorhersehbar und unweigerlich unvollständig: Sie kämpft zum Beispiel vor einer beängstigend aussehenden Rekursion zurück, und das ist wahrscheinlich klug. Eine Sache, über die wir uns keine Sorgen machen müssen, ist die Ausführung von IO Berechnungen im Typechecker: Denken Sie daran, dass der Typechecker nicht launchMissilesdie gleiche Bedeutung haben muss wie das Laufzeitsystem!

Hindley-Milner-Kultur

Das Hindley-Milner-Typ-System erzielt das wirklich beeindruckende Zusammentreffen von vier verschiedenen Unterscheidungen mit dem unglücklichen kulturellen Nebeneffekt, dass viele Menschen die Unterscheidung zwischen den Unterscheidungen nicht erkennen können und davon ausgehen, dass der Zufall unvermeidlich ist! Worüber rede ich?

  • Begriffe gegen Typen
  • explizit geschriebene Dinge gegen implizit geschriebene Dinge
  • Anwesenheit zur Laufzeit vs. Löschen vor der Laufzeit
  • nicht abhängige Abstraktion vs. abhängige Quantifizierung

Wir sind es gewohnt, Begriffe zu schreiben und Typen abzuleiten ... und dann zu löschen. Wir sind es gewohnt, über Typvariablen zu quantifizieren, wobei die entsprechende Typabstraktion und -anwendung still und statisch erfolgt.

Sie müssen nicht zu weit von Vanille Hindley-Milner abweichen, bevor diese Unterscheidungen aus dem Gleichgewicht geraten, und das ist keine schlechte Sache . Zunächst können wir interessantere Typen haben, wenn wir bereit sind, sie an einigen Stellen zu schreiben. In der Zwischenzeit müssen wir keine Wörterbücher für Typklassen schreiben, wenn wir überladene Funktionen verwenden, aber diese Wörterbücher sind zur Laufzeit sicherlich vorhanden (oder inline). In abhängig typisierten Sprachen erwarten wir, dass zur Laufzeit mehr als nur Typen gelöscht werden, aber (wie bei Typklassen) einige implizit abgeleitete Werte nicht gelöscht werden. ZB ist vReplicatedas numerische Argument oft aus dem Typ des gewünschten Vektors ableitbar, aber wir müssen es zur Laufzeit noch kennen.

Welche Sprachdesignoptionen sollten wir überprüfen, da diese Zufälle nicht mehr gelten? Ist es beispielsweise richtig, dass Haskell keine Möglichkeit bietet, einen forall x. tQuantifizierer explizit zu instanziieren ? Wenn der Typechecker nicht xdurch Vereinheitlichen erraten kann t, haben wir keine andere Möglichkeit zu sagen, was sein xmuss.

Im weiteren Sinne können wir "Typinferenz" nicht als ein monolithisches Konzept behandeln, von dem wir entweder alles oder nichts haben. Zunächst müssen wir den Aspekt "Generalisierung" (Milners "let" -Regel), der stark davon abhängt, welche Typen existieren, um sicherzustellen, dass eine dumme Maschine einen erraten kann, vom Aspekt "Spezialisierung" (Milners "var "Regel), die genauso effektiv ist wie Ihr Einschränkungslöser. Wir können davon ausgehen, dass es schwieriger wird, auf Top-Level-Typen zu schließen, aber dass interne Typinformationen relativ einfach zu verbreiten sind.

Nächste Schritte für Haskell

Wir sehen, dass der Typ und die Artstufen sehr ähnlich werden (und sie teilen bereits eine interne Repräsentation in GHC). Wir könnten sie genauso gut zusammenführen. Es würde Spaß machen, * :: *wenn wir können: Wir haben vor langer Zeit die logische Solidität verloren, als wir den Boden zugelassen haben, aber die Typ- Solidität ist normalerweise eine schwächere Anforderung. Wir müssen überprüfen. Wenn wir unterschiedliche Ebenen für Typ, Art usw. haben müssen, können wir zumindest sicherstellen, dass alles auf der Typenebene und darüber immer gefördert werden kann. Es wäre großartig, den Polymorphismus, den wir bereits für Typen haben, wiederzuverwenden, anstatt den Polymorphismus auf der Ebene der Arten neu zu erfinden.

Wir sollten das derzeitige System der Beschränkungen vereinfachen und verallgemeinern, indem wir heterogene Gleichungen zulassen , bei a ~ bdenen die Arten aund bnicht syntaktisch identisch sind (aber als gleich erwiesen werden können). Es ist eine alte Technik (in meiner These vom letzten Jahrhundert), die es viel einfacher macht, mit Abhängigkeiten umzugehen. Wir könnten Einschränkungen für Ausdrücke in GADTs ausdrücken und so Einschränkungen für das, was gefördert werden kann, lockern.

Wir sollten die Notwendigkeit der Singleton-Konstruktion beseitigen, indem wir einen abhängigen Funktionstyp einführen pi x :: s -> t. Eine Funktion mit einem solchen Typ könnte explizit auf jeden Ausdruck des Typs angewendet werden , sder im Schnittpunkt der Typ- und Begriffssprachen lebt (also Variablen, Konstruktoren, mehr dazu später). Das entsprechende Lambda und die entsprechende Anwendung würden zur Laufzeit nicht gelöscht, sodass wir schreiben könnten

vReplicate :: pi n :: Nat -> x -> Vec n x
vReplicate Z     x = VNil
vReplicate (S n) x = VCons x (vReplicate n x)

ohne Natdurch zu ersetzen Natty. Die Domäne von pikann ein beliebiger fördernder Typ sein. Wenn also GADTs gefördert werden können, können wir abhängige Quantifizierersequenzen (oder "Teleskope", wie de Briuijn sie nannte) schreiben.

pi n :: Nat -> pi xs :: Vec n x -> ...

auf welche Länge wir brauchen.

Der Zweck dieser Schritte besteht darin , die Komplexität zu beseitigen, indem direkt mit allgemeineren Werkzeugen gearbeitet wird, anstatt mit schwachen Werkzeugen und klobigen Codierungen auszukommen. Das derzeitige teilweise Buy-In verteuert die Vorteile der abhängigen Typen von Haskell, als sie sein müssen.

Zu schwer?

Abhängige Typen machen viele Menschen nervös. Sie machen mich nervös, aber ich mag es nervös zu sein, oder zumindest fällt es mir schwer, sowieso nicht nervös zu sein. Aber es hilft nicht, dass es um das Thema einen solchen Nebel der Unwissenheit gibt. Einiges davon ist darauf zurückzuführen, dass wir alle noch viel zu lernen haben. Es ist jedoch bekannt, dass Befürworter weniger radikaler Ansätze die Angst vor abhängigen Typen schüren, ohne immer sicherzustellen, dass die Fakten vollständig mit ihnen übereinstimmen. Ich werde keine Namen nennen. Diese "unentscheidbaren Typprüfungen", "Unvollständigen Turing", "Keine Phasenunterscheidung", "Keine Typlöschung", "Beweise überall" usw., Mythen bleiben bestehen, obwohl sie Müll sind.

Es ist sicherlich nicht der Fall, dass abhängig getippte Programme immer als richtig erwiesen werden müssen. Man kann die grundlegende Hygiene seiner Programme verbessern, indem man zusätzliche Invarianten in Typen erzwingt, ohne bis zu einer vollständigen Spezifikation zu gehen. Kleine Schritte in diese Richtung führen häufig zu viel stärkeren Garantien mit wenigen oder keinen zusätzlichen Nachweispflichten. Es ist nicht wahr, dass abhängig typisierte Programme unweigerlich voller Beweise sind, tatsächlich nehme ich normalerweise das Vorhandensein von Beweisen in meinem Code als Anhaltspunkt, um meine Definitionen in Frage zu stellen .

Denn wie bei jeder Erhöhung der Artikulierbarkeit können wir sowohl schlechte als auch faire Dinge sagen. ZB gibt es viele miese Möglichkeiten, binäre Suchbäume zu definieren, aber das bedeutet nicht, dass es keinen guten Weg gibt . Es ist wichtig, nicht anzunehmen, dass schlechte Erfahrungen nicht verbessert werden können, selbst wenn es das Ego dazu bringt, es zuzugeben. Das Entwerfen abhängiger Definitionen ist eine neue Fähigkeit, die Lernen erfordert, und ein Haskell-Programmierer zu sein, macht Sie nicht automatisch zu einem Experten! Und selbst wenn einige Programme schlecht sind, warum würden Sie anderen die Freiheit verweigern, fair zu sein?

Warum immer noch mit Haskell belästigen?

Ich mag abhängige Typen sehr, aber die meisten meiner Hacking-Projekte befinden sich immer noch in Haskell. Warum? Haskell hat Typklassen. Haskell hat nützliche Bibliotheken. Haskell hat eine praktikable (wenn auch alles andere als ideale) Behandlung der Programmierung mit Effekten. Haskell hat einen industriellen Compiler. Die abhängig typisierten Sprachen befinden sich in einem viel früheren Stadium des Wachstums der Community und der Infrastruktur, aber wir werden mit einem echten Generationswechsel dahin gelangen, was möglich ist, z. B. durch Metaprogrammierung und Datentyp-Generika. Aber Sie müssen sich nur umschauen, was die Leute als Ergebnis von Haskells Schritten zu abhängigen Typen tun, um zu sehen, dass es einen großen Vorteil bringt, wenn auch die gegenwärtige Generation von Sprachen vorangebracht wird.

Schweinearbeiter
quelle
6
Das DataKinds-Zeug ist mir wirklich noch egal. Meistens, weil ich so etwas machen möchte : fmap read getLine >>= \n -> vReplicate n 0. Wie Sie bemerken, Nattyist ein Weg davon weg. Darüber hinaus sollte vReplicate in ein tatsächliches Speicherarray übersetzbar sein, etwa dort newtype SVector n x = SVector (Data.Vector.Vector x), wo nArt Nat(oder ähnliches) vorhanden ist. Vielleicht ein weiterer Demonstrationspunkt für einen "abhängig getippten Show-Off"?
John L
7
Können Sie sagen, was Sie für eine ideale Behandlung der Programmierung mit Effekten vorhaben?
Steven Shaw
6
Vielen Dank für das tolle Schreiben. Ich würde gerne einige Beispiele für abhängig typisierten Code sehen, bei denen einige Daten außerhalb des Programms stammen (z. B. aus einer Datei gelesen), um ein Gefühl dafür zu bekommen, wie das Heraufstufen von Werten zu Typen in einer solchen Umgebung aussehen würde. Ich habe das Gefühl, dass alle Beispiele Vektoren (als Listen implementiert) mit statisch bekannten Größen beinhalten.
Tibbe
4
@pigworker Du nimmst "keine Phasenunterscheidung" als Mythos (die anderen, denen ich zustimme, sind Mythen). Aber Sie haben diese in Papieren und Vorträgen, die ich gesehen habe, nicht zerlegt, und währenddessen sagt mir eine andere Person, die ich respektiere: "Die Theorie der abhängigen Typen unterscheidet sich von einem typischen Compiler, weil wir die Phasen der Typprüfung, Kompilierung und Ausführung nicht sinnvoll trennen können. "" (siehe Andrejs neuesten Beitrag vom 8. November 2012) Nach meiner Erfahrung "fälschen" wir manchmal zumindest die Phasenunterscheidung, obwohl wir sie nicht löschen müssen. Könnten Sie dieses Thema erweitern, wenn nicht hier, dann anderswo?
Sclv
4
@sclv Meine Arbeit hat sich nicht speziell mit dem Mythos "keine Phasenunterscheidung" befasst, sondern mit dem anderer. Ich empfehle das Rejectum "Phase Distinctions in the Compilation of Epigram" von James McKinna und Edwin Brady als guten Ausgangspunkt. Siehe aber auch viel ältere Arbeiten zur Programmextraktion in Coq. Die vom Typechecker durchgeführte offene Auswertung ist völlig unabhängig von der Ausführung durch Extraktion in ML, und es ist klar, dass durch die Extraktion Typen und Beweise entfernt werden.
Schweinearbeiter
20

John, das ist ein weiteres häufiges Missverständnis über abhängige Typen: dass sie nicht funktionieren, wenn Daten nur zur Laufzeit verfügbar sind. So können Sie das getLine-Beispiel ausführen:

data Some :: (k -> *) -> * where
  Like :: p x -> Some p

fromInt :: Int -> Some Natty
fromInt 0 = Like Zy
fromInt n = case fromInt (n - 1) of
  Like n -> Like (Sy n)

withZeroes :: (forall n. Vec n Int -> IO a) -> IO a
withZeroes k = do
  Like n <- fmap (fromInt . read) getLine
  k (vReplicate n 0)

*Main> withZeroes print
5
VCons 0 (VCons 0 (VCons 0 (VCons 0 (VCons 0 VNil))))

Edit: Hm, das sollte ein Kommentar zu Pigworkers Antwort sein. Ich scheitere eindeutig bei SO.

ulfnorell
quelle
Ihr erster Satz scheint etwas seltsam; Ich würde sagen , der Punkt abhängiger Arten ist , dass sie tun , arbeiten , wenn Daten zur Laufzeit verfügbar sind. Diese Technik im CPS-Stil ist jedoch nicht dieselbe. Angenommen, Sie haben eine Funktion Vec Zy -> IO String. Sie können es nicht mit verwenden withZeroes, da der Typ Zynicht mit forall n vereinheitlicht werden kann. Vielleicht können Sie das für ein oder zwei Sonderfälle umgehen, aber es gerät schnell außer Kontrolle.
John L
Der Schlüssel, wenn Sie einen einfach eingegebenen Wert (wie den String von getLine) in einen stärkeren Typ (wie einen Natty n oben) umwandeln, besteht darin, dass Sie den Typprüfer davon überzeugen müssen, dass Sie die erforderlichen dynamischen Prüfungen durchführen. In Ihrem Beispiel lesen Sie eine beliebige Zahl, damit dies forall nSinn macht. Genauere Einschränkungen können auf die gleiche Weise implementiert werden. Haben Sie ein besseres Beispiel als Vec Zy(das Programm müsste immer noch den Benutzer behandeln, der 5 statt 0 eingibt)?
ulfnorell
1
Was ich mit dem ersten Satz sagen wollte, ist, dass ich gelegentlich Menschen begegne, die glauben, dass Sie keine abhängigen Typen verwenden können, wenn Sie Ihre Daten durch Interaktion mit der Außenwelt erhalten. Mein Punkt ist, dass Sie nur einen abhängig typisierten Parser schreiben müssen, was normalerweise unkompliziert ist.
ulfnorell
1
ulfnorell: Entschuldigung, ich war nicht klar. Angenommen, Sie haben eine Funktion, mit der funktioniert, Vec Zy -> IO Stringund eine andere für Vec n -> IO String, und Sie möchten die erste nur verwenden, wenn der Typ übereinstimmt. Ja, es ist möglich, aber die Mechanismen zur Aktivierung sind klobig. Und das ist eine sehr einfache Logik; Wenn Sie eine komplexere Logik haben, ist es schlimmer. Außerdem müssen Sie möglicherweise viel Code in CPS neu schreiben. Und Sie haben immer noch keinen Ausdruck
John L
Ah, ich verstehe, was du sagst. Dafür ist Natty da, wie in vReplicate, wo wir abhängig von n verschiedene Dinge tun. In der Tat kann dies etwas klobig werden. Eine Alternative zum CPS-Stil besteht darin, mit gepackten Existentials zu arbeiten : zeroes :: IO (Some (Flip Vec Int)).
ulfnorell
19

Schweinearbeiter gibt eine ausgezeichnete Diskussion darüber, warum wir auf abhängige Typen zusteuern sollten : (a) sie sind großartig; (b) Sie würden tatsächlich vieles vereinfachen, was Haskell bereits tut.

Was das "Warum nicht?" Frage, es gibt ein paar Punkte, denke ich. Der erste Punkt ist, dass der Grundbegriff hinter abhängigen Typen zwar einfach ist (Typen dürfen von Werten abhängen), die Auswirkungen dieses Grundbegriffs jedoch sowohl subtil als auch tiefgreifend sind. Zum Beispiel ist die Unterscheidung zwischen Werten und Typen noch lebendig und gut; aber den Unterschied zwischen ihnen zu diskutieren wird weitnuancierter als in Ihrem Hindley - Milner oder System F. In gewissem Maße ist dies auf die Tatsache zurückzuführen, dass abhängige Typen grundsätzlich schwierig sind (z. B. ist die Logik erster Ordnung unentscheidbar). Aber ich denke, das größere Problem ist wirklich, dass uns ein gutes Vokabular fehlt, um zu erfassen und zu erklären, was los ist. Wenn immer mehr Menschen etwas über abhängige Typen lernen, werden wir ein besseres Vokabular entwickeln und die Dinge leichter verständlich machen, selbst wenn die zugrunde liegenden Probleme immer noch schwierig sind.

Der zweite Punkt hat mit der Tatsache zu tun, dass Haskell ist wächstzu abhängigen Typen. Da wir inkrementelle Fortschritte in Richtung dieses Ziels machen, aber ohne es tatsächlich dort zu schaffen, bleiben wir bei einer Sprache, die inkrementelle Patches zusätzlich zu inkrementellen Patches enthält. Dasselbe ist in anderen Sprachen passiert, als neue Ideen populär wurden. Java hatte früher keinen (parametrischen) Polymorphismus. und als sie es schließlich hinzufügten, war es offensichtlich eine schrittweise Verbesserung mit einigen Abstraktionslecks und verkrüppelter Kraft. Es stellt sich heraus, dass das Mischen von Subtypisierung und Polymorphismus von Natur aus schwierig ist. Dies ist jedoch nicht der Grund, warum Java Generics so funktionieren, wie sie es tun. Sie arbeiten so, wie sie es tun, da sie eine schrittweise Verbesserung gegenüber älteren Java-Versionen darstellen. Das Gleiche gilt für die Zeit, als OOP erfunden wurde und die Leute anfingen, "objektiv" zu schreiben. C (nicht zu verwechseln mit Objective-C) usw. Denken Sie daran, dass C ++ unter dem Deckmantel einer strengen Obermenge von C begann. Um neue Paradigmen hinzuzufügen, muss die Sprache immer neu definiert werden, oder es kommt zu einem komplizierten Durcheinander. Mein Punkt bei all dem ist, dass das Hinzufügen von echten abhängigen Typen zu Haskell ein gewisses Maß an Ausnehmen und Umstrukturieren der Sprache erfordern wird - wenn wir es richtig machen wollen. Aber es ist wirklich schwer, sich auf eine solche Überholung einzulassen, während die schrittweisen Fortschritte, die wir gemacht haben, kurzfristig billiger erscheinen. Wirklich, es gibt nicht so viele Leute, die sich auf GHC hacken, aber es gibt eine Menge Legacy-Code, um am Leben zu bleiben. Dies ist einer der Gründe, warum es so viele Spin-off-Sprachen wie DDC, Cayenne, Idris usw. gibt. C ++ begann unter dem Deckmantel, eine strikte Obermenge von C zu sein. Um neue Paradigmen hinzuzufügen, muss die Sprache immer neu definiert werden, oder es kommt zu einem komplizierten Durcheinander. Mein Punkt bei all dem ist, dass das Hinzufügen von echten abhängigen Typen zu Haskell ein gewisses Maß an Ausnehmen und Umstrukturieren der Sprache erfordern wird - wenn wir es richtig machen wollen. Aber es ist wirklich schwer, sich auf eine solche Überholung einzulassen, während die schrittweisen Fortschritte, die wir gemacht haben, kurzfristig billiger erscheinen. Wirklich, es gibt nicht so viele Leute, die sich auf GHC hacken, aber es gibt eine Menge Legacy-Code, um am Leben zu bleiben. Dies ist einer der Gründe, warum es so viele Spin-off-Sprachen wie DDC, Cayenne, Idris usw. gibt. C ++ begann unter dem Deckmantel, eine strikte Obermenge von C zu sein. Um neue Paradigmen hinzuzufügen, muss die Sprache immer neu definiert werden, oder es kommt zu einem komplizierten Durcheinander. Mein Punkt bei all dem ist, dass das Hinzufügen von echten abhängigen Typen zu Haskell ein gewisses Maß an Ausnehmen und Umstrukturieren der Sprache erfordern wird - wenn wir es richtig machen wollen. Aber es ist wirklich schwer, sich auf eine solche Überholung einzulassen, während die schrittweisen Fortschritte, die wir gemacht haben, kurzfristig billiger erscheinen. Wirklich, es gibt nicht so viele Leute, die sich auf GHC hacken, aber es gibt eine Menge Legacy-Code, um am Leben zu bleiben. Dies ist einer der Gründe, warum es so viele Spin-off-Sprachen wie DDC, Cayenne, Idris usw. gibt. oder am Ende mit einem komplizierten Durcheinander enden. Mein Punkt bei all dem ist, dass das Hinzufügen von echten abhängigen Typen zu Haskell ein gewisses Maß an Ausnehmen und Umstrukturieren der Sprache erfordern wird - wenn wir es richtig machen wollen. Aber es ist wirklich schwer, sich auf eine solche Überholung einzulassen, während die schrittweisen Fortschritte, die wir gemacht haben, kurzfristig billiger erscheinen. Wirklich, es gibt nicht so viele Leute, die sich auf GHC hacken, aber es gibt eine Menge Legacy-Code, um am Leben zu bleiben. Dies ist einer der Gründe, warum es so viele Spin-off-Sprachen wie DDC, Cayenne, Idris usw. gibt. oder am Ende mit einem komplizierten Durcheinander enden. Mein Punkt bei all dem ist, dass das Hinzufügen von echten abhängigen Typen zu Haskell ein gewisses Maß an Ausnehmen und Umstrukturieren der Sprache erfordern wird - wenn wir es richtig machen wollen. Aber es ist wirklich schwer, sich auf eine solche Überholung einzulassen, während die schrittweisen Fortschritte, die wir gemacht haben, kurzfristig billiger erscheinen. Wirklich, es gibt nicht so viele Leute, die sich auf GHC hacken, aber es gibt eine Menge Legacy-Code, um am Leben zu bleiben. Dies ist einer der Gründe, warum es so viele Spin-off-Sprachen wie DDC, Cayenne, Idris usw. gibt. Es ist wirklich schwer, sich auf eine solche Überholung einzulassen, während die inkrementellen Fortschritte, die wir gemacht haben, kurzfristig billiger erscheinen. Wirklich, es gibt nicht so viele Leute, die sich auf GHC hacken, aber es gibt eine Menge Legacy-Code, um am Leben zu bleiben. Dies ist einer der Gründe, warum es so viele Spin-off-Sprachen wie DDC, Cayenne, Idris usw. gibt. Es ist wirklich schwer, sich auf eine solche Überholung einzulassen, während die inkrementellen Fortschritte, die wir gemacht haben, kurzfristig billiger erscheinen. Wirklich, es gibt nicht so viele Leute, die sich auf GHC hacken, aber es gibt eine Menge Legacy-Code, um am Leben zu bleiben. Dies ist einer der Gründe, warum es so viele Spin-off-Sprachen wie DDC, Cayenne, Idris usw. gibt.

Zaunkönig Romano
quelle