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 n
in 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 n
Kopien eines bestimmten x
(das wäre pure
zu 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 n
ist 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 Nat
drei 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 n
Werte und Nattily n
Wörterbücher. Darüber hinaus Natty
ist 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 Nat
kann zwar gefördert werden, Vec
kann 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 x
und
VCons z zs :: Vec (S m) x
wo 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 family
kö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 Nat
mit 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 Monad
werden 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
launchMissiles
die 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 vReplicate
das 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. t
Quantifizierer explizit zu instanziieren ? Wenn der Typechecker nicht x
durch Vereinheitlichen erraten kann t
, haben wir keine andere Möglichkeit zu sagen, was sein x
muss.
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 ~ b
denen die Arten a
und
b
nicht 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 , s
der 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 Nat
durch zu ersetzen Natty
. Die Domäne von pi
kann 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.