Wenn etwas einfach ist, sollte es mit ein paar Worten vollständig erklärbar sein. Dies kann für die λ-Rechnung erfolgen:
Der λ-Kalkül ist eine syntaktische Grammatik (im Grunde genommen eine Struktur) mit einer Verkleinerungsregel (was bedeutet, dass eine Such- / Ersetzungsprozedur wiederholt auf jedes Auftreten eines bestimmten Musters angewendet wird, bis kein solches Muster existiert).
Grammatik:
Term = (Term Term) | (λ Var . Term) | Var
Reduzierungsregel:
((λ var body) term) -> SUBS(body,var,term) where `SUBS` replaces all occurrences of `var` by `term` in `body`, avoiding name capture.
Beispiele:
(λ a . a) -> (λ a a) ((λ a . (λ b . (b a))) (λ x . x)) -> (λ b . (b (λ x x))) ((λ a . (a a)) (λ x . x)) -> (λ x . x) ((λ a . (λ b . ((b a) a))) (λ x . x)) -> (λ b . ((b (λ x . x)) (λ x . x))) ((λ x . (x x)) (λ x . (x x))) -> never halts
Obwohl dies etwas informell ist, könnte man behaupten, dass dies informativ genug ist, damit ein normaler Mensch den λ-Kalkül als Ganzes versteht - und es dauert 22 Zeilen Abschlag. Ich versuche, reine / abhängige Typsysteme, wie sie von Idris / Agda und ähnlichen Projekten verwendet werden, zu verstehen , aber die kürzere Erklärung, die ich finden konnte, war Simply Easy - eine großartige Arbeit, die jedoch eine Menge Vorkenntnisse vorauszusetzen scheint (Haskell, induktiv) Definitionen), die ich nicht habe. Ich denke, etwas kürzeres, weniger reiches könnte einige dieser Barrieren beseitigen. Somit,
Ist es möglich, eine kurze, vollständige Erklärung von reinen / abhängigen Typsystemen in demselben Format zu geben, in dem ich die λ-Rechnung oben dargestellt habe?
quelle
Antworten:
Haftungsausschluss
Dies ist sehr informell, wie Sie es gewünscht haben.
Die Grammatik
In einer abhängig getippten Sprache haben wir einen Ordner sowohl auf der Typebene als auch auf der Wertebene:
Ein gut typisierter Begriff ist ein Begriff mit angehängter Schrift, den wir
t ∈ σ
oder schreiben werdenum anzuzeigen, dass der Begriff
t
einen Typ hatσ
.Tippregeln
Der Einfachheit halber fordern wir das in
λ v. t ∈ ∀ (v : σ). τ
beidenλ
und∀
binden die gleiche Variable (v
in diesem Fall).Regeln:
Ist
*
also "der Typ aller Typen" (1),∀
bildet Typen aus Typen (2), Lambda-Abstraktionen haben pi-Typen (3) und wennv
durch eingeführt∀ (v : σ). τ
, dannv
Typσ
(5)."in normaler Form" bedeutet, dass wir so viele Reduzierungen wie möglich mit der Reduzierungsregel durchführen:
"Die" Reduktionsregel
Oder in zweidimensionaler Syntax wo
bedeutet
t ∈ σ
:Es ist nur möglich, eine Lambda-Abstraktion auf einen Term anzuwenden, wenn der Term den gleichen Typ wie die Variable im zugeordneten forall-Quantifizierer hat. Dann reduzieren wir sowohl die Lambda-Abstraktion als auch den Forall-Quantifikator auf die gleiche Weise wie zuvor im reinen Lambda-Kalkül. Nach dem Subtrahieren des Wertebenenteils erhalten wir die Typisierungsregel (4).
Ein Beispiel
Hier ist der Funktionsanwendungsoperator:
(wir kürzen
∀ (x : σ). τ
aufσ -> τ
wennτ
nicht erwähntx
)f
GibtB y
für jeden bereitgestellteny
Typ zurückA
. Wir wendenf
aufx
, die von der richtigen Art istA
, und Ersatzy
fürx
die∀
nach.
, sof x ∈ SUBS(B y, y, x)
~>f x ∈ B x
.Kürzen wir nun den Funktionsanwendungsoperator als
app
und wenden ihn auf sich selbst an:Ich stelle
?
Begriffe auf, die wir liefern müssen. Zuerst führen wir explizit ein und instanziierenA
undB
:Jetzt müssen wir vereinheitlichen, was wir haben
das ist das gleiche wie
und was
app ? ?
erhältDas führt zu
(Siehe auch Was ist Prädikativität? )
Unser Ausdruck (nach einigem Umbenennen) wird
Da für jeden
A
,B
undf
(wobeif ∈ ∀ (y : A). B y
)wir können instanziieren
A
undB
bekommen (für jedenf
mit dem passenden Typ)und die Typensignatur ist äquivalent zu
(∀ (x : A). B x) -> ∀ (x : A). B x
.Der ganze Ausdruck ist
Dh
was nach allen Reduzierungen auf dem Wertniveau das gleiche
app
zurückgibt.Während es also nur ein paar Schritte in der reinen Lambda-Rechnung erfordert, um
app
davon zu kommenapp app
, müssen wir uns in einer getippten Umgebung (und insbesondere einer abhängig getippten) auch um die Vereinheitlichung kümmern und die Dinge werden komplexer, selbst mit einigem inkonsequenten Komfort (* ∈ *
).Typüberprüfung
t
ist*
dannt ∈ *
von (1)t
ist∀ (x : σ) τ
,σ ∈? *
,τ ∈? *
(siehe die Notiz∈?
unten) dannt ∈ *
durch (2)t
istf x
,f ∈ ∀ (v : σ) τ
für einigeσ
undτ
,x ∈? σ
dannt ∈ SUBS(τ, v, x)
durch (4)t
es sich um eine Variable handeltv
,v
wurde bis∀ (v : σ). τ
dahint ∈ σ
durch (5) eingeführt.Dies sind alles Inferenzregeln, aber wir können nicht dasselbe für Lambdas tun (Typinferenz ist für abhängige Typen unentscheidbar). Für Lambdas prüfen wir also (
t ∈? σ
), anstatt daraus zu schließen:t
istλ v. b
und gegen geprüft∀ (v : σ) τ
,b ∈? τ
dannt ∈ ∀ (v : σ) τ
t
es sich um etwas anderes handelt und gegen das geprüftσ
wird, schließen Sie die Art dert
Verwendung der obigen Funktion ab und prüfen Sie, ob dies der Fall istσ
Die Gleichheitsprüfung für Typen erfordert, dass sie in normalen Formen vorliegen. Um zu entscheiden, ob
t
der Typ vorhanden istσ
, überprüfen wir zunächst , ob der Typ vorhandenσ
ist*
. Wenn ja, dannσ
ist es normalisierbar (Modulo-Girard-Paradoxon) und es wird normalisiert (daherσ
wird es durch (0) gut geformt).SUBS
normalisiert auch zu bewahrende Ausdrücke (0).Dies wird als bidirektionale Typprüfung bezeichnet. Damit müssen wir nicht jedes Lambda mit einem Typ versehen: Wenn
f x
der Typ vonf
bekannt ist,x
wird er gegen den Typ desf
empfangenen Arguments geprüft, anstatt abgeleitet und auf Gleichheit verglichen zu werden (was ebenfalls weniger effizient ist). Wennf
es sich jedoch um ein Lambda handelt, ist eine explizite Typanmerkung erforderlich (Anmerkungen werden in der Grammatik weggelassen, und Sie können sie überall entweder den Konstruktoren hinzufügenAnn Term Term
oder hinzufügenλ' (σ : Term) (v : Var)
).Schauen Sie sich auch den einfacheren an! Blogeintrag.
quelle
(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ)
~>SUBS(b, v, t) ∈ SUBS(τ, v, t)
.(∀ (v : σ). τ) t ~> ...
eine andere für den Sinn(λ v. b) t ~> ...
. Ich würde das erste entfernen und es in einen Kommentar unten verwandeln.Lass uns gehen. Ich werde mich nicht um Girards Paradox kümmern, weil es von den zentralen Ideen ablenkt. Ich muss eine Präsentationsmaschinerie über Urteile und Ableitungen und dergleichen einführen.
Grammatik
Die Grammatik hat zwei sich gegenseitig definierende Formen, "Ausdrücke", die den allgemeinen Begriff der Sache darstellen (Typen sind Dinge, Werte sind Dinge), einschließlich * (Typ der Typen), abhängiger Funktionstypen und Lambda-Abstraktionen, aber auch Einbettung. Eliminierungen "(dh" Verwendungen "anstelle von" Konstruktionen "), die verschachtelte Anwendungen sind, bei denen das Objekt, das sich letztendlich an der Funktionsposition befindet, entweder eine Variable oder ein Begriff ist, der mit seinem Typ versehen ist.
Reduzierungsregeln
Die Substitutionsoperation t [e / x] ersetzt jedes Vorkommen der Variablen x durch die Elimination e, wodurch die Erfassung von Namen vermieden wird. Um eine Anwendung zu bilden, die reduziert werden kann, muss ein Lambda- Term nach Typ mit Anmerkungen versehen werden, um eine Eliminierung vorzunehmen . Die Typanmerkung verleiht der Lambda-Abstraktion eine Art "Reaktivität", wodurch die Anwendung fortgesetzt werden kann. Sobald wir den Punkt erreicht haben, an dem keine Anwendung mehr stattfindet und das aktive t: T wieder in die Ausdruckssyntax eingebettet wird, können wir die Typanmerkung löschen.
Lassen Sie uns die ↝ -Reduktionsrelation durch strukturellen Abschluss erweitern: Die Regeln gelten überall innerhalb von Begriffen und Eliminierungen, sodass Sie etwas finden, das mit der linken Seite übereinstimmt. Schreiben Sie ↝ * für den reflexiv-transitiven (0-oder-mehrstufigen) Abschluss von ↝. Das resultierende Reduktionssystem ist in diesem Sinne konfluent:
Kontexte
Kontexte sind Sequenzen, die rechts wachsenden Variablen Typen zuweisen, die wir als "lokales" Ende betrachten und die uns über die zuletzt gebundenen Variablen informieren. Eine wichtige Eigenschaft von Kontexten ist, dass es immer möglich ist, eine Variable auszuwählen, die noch nicht im Kontext verwendet wird. Wir behalten die Invariante bei, dass die den Variablen im Kontext zugewiesenen Typen unterschiedlich sind.
Urteile
Das ist die Grammatik der Urteile, aber wie liest man sie? Zunächst ist ⊢ das traditionelle "Drehkreuz" -Symbol, das Annahmen von Schlussfolgerungen trennt: Sie können es informell lesen, wie "sagt".
bedeutet, dass bei gegebenem Kontext G der Typ T den Begriff t zulässt;
bedeutet, dass bei gegebenem Kontext G die Elimination e vom Typ S ist.
Urteile haben eine interessante Struktur: keine oder mehrere Eingaben , ein oder mehrere Subjekte , keine oder mehrere Ausgaben .
Das heißt, wir müssen die Arten von Begriffen im Voraus vorschlagen und sie nur überprüfen , aber wir synthetisieren die Arten von Eliminierungen.
Tippregeln
Ich präsentiere diese in einem vagen Prologstil und schreibe J -: P1; ...; Pn, um anzuzeigen, dass die Beurteilung J gilt, wenn die Prämissen P1 bis Pn ebenfalls gelten. Eine Prämisse ist ein anderes Urteil oder ein Anspruch auf Minderung.
Und das ist es!
Nur zwei Regeln richten sich nicht nach der Syntax: Die Regel "Sie können einen Typ reduzieren, bevor Sie ihn zum Überprüfen eines Begriffs verwenden" und die Regel "Sie können einen Typ reduzieren, nachdem Sie ihn aus einer Elimination synthetisiert haben". Eine sinnvolle Strategie besteht darin, Typen zu reduzieren, bis Sie den obersten Konstruktor verfügbar gemacht haben.
Dieses System normalisiert sich nicht stark (aufgrund von Girards Paradoxon, das ein lügnerisches Paradoxon der Selbstreferenz ist), aber es kann stark normalisiert werden, indem man * in "Universumsebenen" aufteilt, in denen Werte enthalten sind, die Typen auf niedrigeren Ebenen selbst betreffen Typen auf höheren Ebenen haben, die Selbstreferenz verhindern.
Dieses System hat jedoch in diesem Sinne die Eigenschaft der Typenerhaltung.
Kontexte können berechnet werden, indem die darin enthaltenen Begriffe berechnet werden. Das heißt, wenn ein Urteil jetzt gültig ist, können Sie seine Eingaben so oft berechnen, wie Sie möchten, und sein Thema in einem Schritt. Dann können Sie seine Ausgaben irgendwie berechnen, um sicherzustellen, dass das resultierende Urteil gültig bleibt. Der Beweis ist eine einfache Induktion auf Typisierungsableitungen, gegeben dem Zusammenfluss von -> *.
Natürlich habe ich hier nur den Funktionskern vorgestellt, aber Erweiterungen können recht modular sein. Hier sind Paare.
quelle
G, x:S, G' ⊢ x is S -: G' ⊬ x
?Die Curry-Howard-Entsprechung besagt, dass es eine systematische Entsprechung zwischen Typsystemen und Beweissystemen in der Logik gibt. Wenn Sie dies programmiererzentriert betrachten, können Sie es folgendermaßen umformulieren:
Aus diesem Blickwinkel gesehen:
Hier sind die Regeln des natürlichen Abzugs für die Logik erster Ordnung unter Verwendung eines Diagramms aus dem Wikipedia-Eintrag zum natürlichen Abzug . Dies sind im Grunde genommen auch die Regeln eines minimal abhängig typisierten Lambda-Kalküls.
Beachten Sie, dass die Regeln Lambda-Terme enthalten. Diese können als die Programme gelesen werden, die die Beweise der Sätze bilden, die durch ihre Typen dargestellt werden (oder genauer gesagt, wir sagen nur, dass Programme Beweise sind ). Ähnliche Reduktionsregeln, die Sie angeben, können auf diese Lambda-Terme angewendet werden.
Warum interessiert uns das? Zunächst einmal, denn Beweise können sich als nützliches Programmierwerkzeug herausstellen, und eine Sprache, die mit Beweisen als erstklassige Objekte arbeiten kann, eröffnet viele Möglichkeiten. Wenn Ihre Funktion beispielsweise eine Vorbedingung hat, können Sie, anstatt sie als Kommentar aufzuschreiben, tatsächlich einen Beweis als Argument verlangen.
Zweitens, weil die Typsystem-Maschinerie, die zum Behandeln von Quantifizierern benötigt wird, in einem Programmierkontext andere Verwendungen haben kann. Insbesondere verarbeiten abhängig typisierte Sprachen Universalquantifikatoren ("für alle x, ...") mithilfe eines Konzepts, das als abhängige Funktionstypen bezeichnet wird - eine Funktion, bei der der statische Typ des Ergebnisses vom Laufzeitwert des Arguments abhängen kann .
Um dies für Fußgänger zu vereinfachen, schreibe ich die ganze Zeit Code, um Avro- Dateien zu lesen , die aus Datensätzen mit einheitlicher Struktur bestehen - alle haben denselben Satz von Feldnamen und -typen. Dafür muss ich entweder:
Wie Sie auf der Tutorial-Seite von Avro Java sehen können , zeigen sie Ihnen, wie Sie die Bibliothek gemäß diesen beiden Ansätzen verwenden.
Mit abhängigen Funktionstypen können Sie Ihren Kuchen auf Kosten eines komplexeren Typensystems zu sich nehmen und essen. Sie können eine Funktion schreiben, die eine Avro-Datei liest, das Schema extrahiert und den Inhalt der Datei als Datenstrom von Datensätzen zurückgibt, deren statischer Typ vom in der Datei gespeicherten Schema abhängt . Der Compiler kann Fehler abfangen, wenn ich beispielsweise versuche, auf ein benanntes Feld zuzugreifen, das möglicherweise nicht in den Datensätzen der Dateien vorhanden ist, die zur Laufzeit verarbeitet werden. Süß, nicht wahr?
quelle