Ein unterer Typ ist ein Konstrukt, das hauptsächlich in der mathematischen Typentheorie vorkommt. Es wird auch als leerer Typ bezeichnet. Es ist ein Typ, der keine Werte hat, aber ein Subtyp aller Typen.
Wenn der Rückgabetyp einer Funktion der unterste Typ ist, bedeutet dies, dass sie nicht zurückgegeben wird. Zeitraum. Vielleicht schleift es für immer, oder es löst eine Ausnahme aus.
Was bringt es, diesen seltsamen Typ in einer Programmiersprache zu haben? Es ist nicht so verbreitet, aber es ist in einigen wie Scala und Lisp vorhanden.
programming-languages
type-systems
GregRos
quelle
quelle
void
Daten explizit definieren ...void
, und der Einheitentyp muss einen Wert haben. Wie Sie bereits betont haben, können Sie nicht einmal einen Wert vom Typ deklarierenvoid
, das heißt, es handelt sich nicht einmal um einen Typ, sondern nur um einen speziellen Eckfall in der Sprache.void
in Java ist es fast dasselbe: Nicht wirklich ein Typ und kann keine Werte haben.nil
(aka,()
), das ein Einheitentyp ist.Antworten:
Ich nehme ein einfaches Beispiel: C ++ vs Rust.
Hier ist eine Funktion zum Auslösen einer Ausnahme in C ++ 11:
Und hier ist das Äquivalent in Rust:
Rein syntaktisch ist das Rust-Konstrukt sinnvoller. Beachten Sie, dass das C ++ - Konstrukt einen Rückgabetyp angibt , obwohl es auch angibt, dass er nicht zurückgegeben wird. Das ist ein bisschen komisch.
Standardmäßig erschien die C ++ - Syntax nur mit C ++ 11 (sie wurde oben angeheftet), aber verschiedene Compiler stellten seit einiger Zeit verschiedene Erweiterungen bereit, so dass Analysewerkzeuge von Drittanbietern programmiert werden mussten, um die verschiedenen Möglichkeiten zu erkennen Dieses Attribut könnte geschrieben werden. Es standardisiert zu haben, ist offensichtlich eindeutig überlegen.
Nun zum Nutzen?
Die Tatsache, dass eine Funktion nicht zurückgegeben wird, kann nützlich sein für:
quelle
void
In Ihrem C ++ - Beispiel wird der Typ der Funktion (ein Teil davon) definiert, nicht der Rückgabetyp. Dies schränkt den Wert ein, den die Funktion annehmen darfreturn
. alles, was sich ins Leere verwandeln lässt (was nichts ist). Wenn die Funktionreturn
s ist, darf kein Wert folgen. Der vollständige Typ der Funktion istvoid () (char const*, char const*, int, char const *)
. + 1 für die Verwendungchar const
anstelle vonconst char
:-)[[noreturn]]
par der Syntax oder einem Zusatz von Funktionalität?Karls Antwort ist gut. Hier ist eine zusätzliche Verwendung, von der ich glaube, dass sie noch niemand erwähnt hat. Die Art von
sollte ein Typ sein, der alle Werte im Typ von
A
und alle Werte im Typ von enthältB
. Wenn der Typ vonB
istNothing
, kann der Typ desif
Ausdrucks der Typ von seinA
. Ich werde oft eine Routine erklärenzu sagen, dass der Code voraussichtlich nicht erreicht wird. Da es sich um den Typ handelt
Nothing
,unreachable(s)
kann es jetzt in jedemif
oder (häufiger) verwendet werden,switch
ohne dass die Art des Ergebnisses beeinflusst wird. Zum BeispielScala hat so einen Nichts-Typ.
Ein weiterer Anwendungsfall für
Nothing
(wie in Karls Antwort erwähnt) ist List [Nothing]. Dabei handelt es sich um die Art von Listen, deren Mitglieder jeweils den Typ Nothing haben. Es kann also der Typ der leeren Liste sein.Die Schlüsseleigenschaft
Nothing
, die diese Anwendungsfälle funktionieren lässt, ist nicht, dass sie keine Werte haben - obwohl sie beispielsweise in Scala keine Werte haben -, dass sie ein Untertyp jedes anderen Typs sind.Angenommen, Sie haben eine Sprache, in der jeder Typ denselben Wert enthält - nennen wir es
()
. In einer solchen Sprache kann der Einheitentyp, der()
den einzigen Wert hat, ein Untertyp jedes Typs sein. Das macht es nicht zu einem Bottom-Typ im Sinne des OP; Dem OP war klar, dass ein Bottom-Typ keine Werte enthält. Da es sich jedoch um einen Typ handelt, der ein Untertyp jedes Typs ist, kann er in etwa die gleiche Rolle spielen wie ein unterer Typ.Haskell macht die Dinge ein bisschen anders. In Haskell kann ein Ausdruck, der niemals einen Wert erzeugt, das Typschema haben
forall a.a
. Eine Instanz dieses Typschemas wird mit jedem anderen Typ vereinheitlicht, sodass sie effektiv als unterer Typ fungiert, obwohl (Standard-) Haskell keine Vorstellung von Untertypisierung hat. Beispielsweise hat dieerror
Funktion aus dem Standardvorspiel ein Typschemaforall a. [Char] -> a
. Du kannst also schreibenund der Typ des Ausdrucks
A
ist für jeden Ausdruck der gleiche wie der Typ vonA
.Die leere Liste in Haskell hat das Typschema
forall a. [a]
. WennA
ein Ausdruck ist, dessen Typ ein Listentyp ist, dannist ein Ausdruck mit dem gleichen Typ wie
A
.quelle
forall a . [a]
und dem Typ[a]
in Haskell? Sind Typvariablen in Haskell-Typausdrücken nicht bereits allgemein quantifiziert?forall
in Standard-Haskell 2010 schreiben . Ich habe die Quantifizierung explizit geschrieben, da dies kein Haskell-Forum ist und einige Leute möglicherweise nicht mit Haskells Konventionen vertraut sind. Es gibt also keinen Unterschied, außer dass diesforall a . [a]
kein Standard[a]
ist.Typen bilden auf zwei Arten ein Monoid und bilden zusammen ein Semiring . Das nennt man algebraische Datentypen . Für endliche Typen bezieht sich dieses Semiren direkt auf das Semiren natürlicher Zahlen (einschließlich Null). Dies bedeutet, dass Sie zählen, wie viele mögliche Werte der Typ hat (ohne „nicht terminierende Werte“).
Vacuous
) hat Nullwerte † .()
.(Bool, Bool)
hat vier mögliche Werte, nämlich(False,False)
,(False,True)
,(True,False)
und(True,True)
.Der Einheitentyp ist das Identitätselement der Kompositionsoperation. ZB
((), False)
und((), True)
sind die einzigen Werte von Typ((), Bool)
, daher ist dieser Typ für sichBool
selbst isomorph .A
und hat imB
Grunde alle Werte vonA
und alle Werte vonB
, daher Summentyp . Zum BeispielEither () Bool
hat drei Werte, ich werde sie rufenLeft ()
,Right False
undRight True
.Der unterste Typ ist das Identitätselement der Summe:
Either Vacuous A
enthält nur Werte der FormRight a
, da erLeft ...
keinen Sinn ergibt (Vacuous
enthält keine Werte).Das Interessante an diesen Monoiden ist, dass, wenn Sie Funktionen in Ihre Sprache einführen , die Kategorie dieser Typen mit den Funktionen als Morphismen eine monoidale Kategorie ist . Hiermit können Sie unter anderem anwendbare Funktoren und Monaden definieren , die sich als hervorragende Abstraktion für allgemeine Berechnungen (möglicherweise mit Nebenwirkungen usw.) in ansonsten rein funktionalen Begriffen herausstellen.
Tatsächlich kann man es ziemlich weit bringen, sich nur um eine Seite des Problems zu kümmern (das Kompositionsmonoid), dann braucht man den Bottom-Typ nicht wirklich explizit. Zum Beispiel hatte selbst Haskell lange Zeit keinen Standardbodentyp. Jetzt heißt es
Void
.Betrachtet man aber das Gesamtbild als eine bikartesische geschlossene Kategorie , so entspricht das Typensystem tatsächlich der gesamten Lambda-Rechnung, so hat man im Grunde die perfekte Abstraktion über alles Mögliche in einer Turing-vollständigen Sprache. Ideal für eingebettete domänenspezifische Sprachen, zum Beispiel gibt es ein Projekt zur direkten Kodierung elektronischer Schaltungen auf diese Weise .
Natürlich kann man sagen, dass dies der allgemeine Unsinn aller Theoretiker ist . Sie müssen sich überhaupt nicht mit Kategorietheorie auskennen, um ein guter Programmierer zu sein, aber wenn Sie dies tun, erhalten Sie mächtige und lächerlich allgemeine Möglichkeiten, um über Code zu argumentieren und Invarianten zu beweisen.
† mb21 erinnert mich daran, dass dies nicht mit den unteren Werten verwechselt werden sollte . In faulen Sprachen wie Haskell enthält jeder Typ einen unteren "Wert", der als "Wert" bezeichnet wird
⊥
. Dies ist keine konkrete Sache, die Sie jemals explizit weitergeben könnten, sondern das, was beispielsweise "zurückgegeben" wird, wenn eine Funktion für immer eine Schleife durchläuft. Sogar HaskellsVoid
Typ enthält den untersten Wert, also den Namen. In diesem Licht hat Haskells unterster Typ tatsächlich einen Wert und sein Einheitentyp zwei Werte, aber in der kategorietheoretischen Diskussion wird dies im Allgemeinen ignoriert.quelle
Void
)", der nicht mit dem Wert zu verwechseln istbottom
, der ein Mitglied eines beliebigen Typs in Haskell ist .Klingt nach einem nützlichen Typ, den man in solchen Situationen haben kann, auch wenn sie selten sind.
Auch wenn
Nothing
(Scalas Name für den untersten Typ) keine WerteList[Nothing]
haben kann, unterliegt diese Einschränkung nicht, weshalb sie als Typ einer leeren Liste nützlich ist. Die meisten Sprachen umgehen dies, indem sie eine leere Liste von Zeichenfolgen anders als eine leere Liste von Ganzzahlen formatieren. Dies ist zwar sinnvoll, führt jedoch zu einer ausführlicheren Schreibweise einer leeren Liste, was in einer listenorientierten Sprache ein großer Nachteil ist.quelle
[]
stellen jedoch alle von ihnen dar und werden instaniert den spezifischen Typ nach Bedarf.[a]
. Ebenso:t Left 1
AusbeutenNum a => Either a b
. Die tatsächliche Bewertung des Ausdrucks erzwingt die Art vona
, aber nicht vonb
:Either Integer b
forall
in ihrem Typ implizierteforall a. [a]
. Es gibt ein paar gute Möglichkeiten, darüber nachzudenkenforall
, aber es braucht einige Zeit, um es wirklich herauszufinden.*
.[]
handelt es sich um einen Typkonstruktor und[]
einen Ausdruck, der eine leere Liste darstellt. Dies bedeutet jedoch nicht, dass "Haskells leere Liste ein Typkonstruktor ist". Der Kontext macht deutlich, ob[]
er als Typ oder als Ausdruck verwendet wird. Angenommen, Sie erklärendata Foo x = Foo | Bar x (Foo x)
; Jetzt können SieFoo
es als Typkonstruktor oder als Wert verwenden, aber zufällig haben Sie für beide denselben Namen gewählt.Für die statische Analyse ist es hilfreich zu dokumentieren, dass ein bestimmter Codepfad nicht erreichbar ist. Wenn Sie beispielsweise Folgendes in C # schreiben:
Der Compiler wird sich beschweren, dass
F
in mindestens einem Codepfad nichts zurückgegeben wird. WennAssert
der Compiler als nicht zurückgegeben markiert werden soll, muss er nicht warnen.quelle
In einigen Sprachen
null
hat der Bottom-Typ, da der Subtyp aller Typen genau definiert, für welche Sprachen null verwendet wird (trotz des milden Widerspruchs,null
sowohl sich selbst als auch eine Funktion zu haben, die sich selbst zurückgibt, ohne die allgemeinen Argumente darüber, warumbot
unbewohnt sein sollte).Es kann auch als
any -> bot
Sammelbegriff für Funktionstypen ( ) verwendet werden, um einen fehlerhaften Versand zu behandeln.In einigen Sprachen können Sie tatsächlich
bot
einen Fehler beheben , der verwendet werden kann, um benutzerdefinierte Compilerfehler bereitzustellen.quelle
void
in gemeinsamen Sprachen abgebildet (allerdings mit geringfügig unterschiedlicher Semantik für die gleiche Verwendung), nichtnull
. Sie haben auch Recht, dass die meisten Sprachen nicht null als untersten Typ modellieren.null
zB einen Zeiger vergleichen,null
um ein Boolesches Ergebnis zu erhalten. Ich denke, die Antworten zeigen, dass es zwei verschiedene Arten von Grundtypen gibt. (a) Sprachen (z. B. Scala), in denen der Typ, der ein Untertyp jedes Typs ist, Berechnungen darstellt, die keine Ergebnisse liefern. Im Grunde ist es ein leerer Typ, obwohl er technisch häufig mit einem nutzlosen Grundwert gefüllt ist, der Nichtbeeinflussung darstellt. (b) Sprachen wie Tangens, in denen der unterste Typ eine Teilmenge jedes anderen Typs ist, weil er einen nützlichen Wert enthält, der auch in jedem anderen Typ enthalten ist - null.Ja, das ist ein ziemlich nützlicher Typ. Während seine Rolle größtenteils innerhalb des Typensystems liegt, gibt es einige Fälle, in denen der unterste Typ offen auftaucht.
Stellen Sie sich eine statisch typisierte Sprache vor, in der Bedingungen Ausdrücke sind (die if-then-else-Konstruktion fungiert also auch als ternärer Operator von C und friends, und es kann eine ähnliche Multi-Way-Case-Anweisung geben). Funktionale Programmiersprachen haben dies, aber es kommt auch in bestimmten imperativen Sprachen vor (seit ALGOL 60). Dann müssen alle Verzweigungsausdrücke letztendlich den Typ des gesamten bedingten Ausdrucks erzeugen. Man könnte einfach verlangen, dass ihre Typen gleich sind (und ich denke, dass dies für den ternären Operator in C der Fall ist), aber dies ist zu restriktiv, insbesondere, wenn die Bedingung auch als bedingte Anweisung verwendet werden kann (und keinen nützlichen Wert zurückgibt). Im Allgemeinen möchte man, dass jeder Verzweigungsausdruck (implizit) konvertierbar ist zu einem gemeinsamen Typ, der der Typ des vollständigen Ausdrucks ist (möglicherweise mit mehr oder weniger komplizierten Einschränkungen, damit der Complier diesen gemeinsamen Typ effektiv findet, vgl. C ++, aber ich werde hier nicht auf diese Details eingehen).
Es gibt zwei Arten von Situationen, in denen eine allgemeine Art der Konvertierung die notwendige Flexibilität solcher bedingten Ausdrücke ermöglicht. Eines ist bereits erwähnt, wobei der Ergebnistyp der Einheitentyp ist
void
; Dies ist natürlich ein Supertyp aller anderen Typen, und die (triviale) Konvertierung jedes beliebigen Typs ermöglicht die Verwendung des bedingten Ausdrucks als bedingte Anweisung. Das andere Problem betrifft Fälle, in denen der Ausdruck zwar einen nützlichen Wert zurückgibt, einer oder mehrere Zweige jedoch keinen erzeugen können. Sie lösen normalerweise eine Ausnahme aus oder beinhalten einen Sprung, und es wäre sinnlos, wenn sie (auch) einen Wert des Typs des gesamten Ausdrucks (von einem nicht erreichbaren Punkt aus) erzeugen müssten. Es ist diese Art von Situation, die angemessen gehandhabt werden kann, indem Ausnahmebedingungsklauseln, Sprünge und Aufrufe gegeben werden, die einen solchen Effekt haben, nämlich den untersten Typ, den man (trivial) in einen anderen Typ umwandeln kann.Ich würde vorschlagen, einen solchen Bottom-Typ
*
zu schreiben , um dessen Konvertierbarkeit in einen beliebigen Typ vorzuschlagen. Es kann intern anderen nützlichen Zwecken dienen, zum Beispiel beim Versuch, einen Ergebnistyp für eine rekursive Funktion abzuleiten, die keine deklariert. Der Typinferencer könnte den Typ*
einem beliebigen rekursiven Aufruf zuweisen , um eine Henne-Ei-Situation zu vermeiden. Der tatsächliche Typ wird durch nicht rekursive Zweige bestimmt, und die rekursiven werden in den allgemeinen Typ der nicht rekursiven Zweige konvertiert. Wenn es überhaupt keine nicht-rekursiven Verzweigungen*
gibt, bleibt der Typ erhalten und zeigt korrekt an, dass es für die Funktion nicht möglich ist, jemals von der Rekursion zurückzukehren. Davon abgesehen und als Ergebnistyp für Ausnahmefunktionen kann man verwenden*
als Komponententyp von Folgen der Länge 0, zum Beispiel der leeren Liste; Nochmals, wenn jemals ein Element aus einem Ausdruck vom Typ[*]
(unbedingt leere Liste) ausgewählt wird, zeigt der resultierende Typ*
korrekt an, dass dies niemals ohne Fehler zurückgegeben werden kann.quelle
var foo = someCondition() ? functionReturningBar() : functionThatAlwaysThrows()
auf die Art vonfoo
as schließenBar
, da der Ausdruck niemals etwas anderes ergeben könnte?void
in C als "return" deklariert wurde. Der zweite Teil Ihrer Antwort, in dem Sie über einen Typ für eine Funktion sprechen, die niemals zurückgibt, oder eine Liste ohne Elemente - das ist in der Tat der unterste Typ! (Es wird oft_|_
eher als geschrieben als*
. Ich weiß nicht warum. Vielleicht, weil es wie ein (menschlicher)functionThatAlwaysThrows()
durch eine explizite ersetzt wurdenthrow
, durch spezielle Sprache in der Norm. Einen Typ zu haben, der dies tut, wäre eine Verbesserung.In einigen Sprachen können Sie eine Funktion mit Anmerkungen versehen, um sowohl dem Compiler als auch den Entwicklern mitzuteilen, dass ein Aufruf dieser Funktion nicht zurückgegeben wird (und wenn die Funktion so geschrieben ist, dass sie zurückgegeben werden kann, lässt der Compiler dies nicht zu ). Das ist eine nützliche Sache zu wissen, aber am Ende können Sie eine solche Funktion wie jede andere aufrufen. Der Compiler kann die Informationen zur Optimierung verwenden, um Warnungen über toten Code auszugeben usw. Es gibt also keinen sehr zwingenden Grund, diesen Typ zu haben, aber auch keinen sehr zwingenden Grund, ihn zu vermeiden.
In vielen Sprachen kann eine Funktion "void" zurückgeben. Was das genau bedeutet, hängt von der Sprache ab. In C bedeutet dies, dass die Funktion nichts zurückgibt. In Swift bedeutet dies, dass die Funktion ein Objekt mit nur einem möglichen Wert zurückgibt. Da es nur einen möglichen Wert gibt, benötigt dieser Wert null Bits und erfordert keinen Code. In beiden Fällen ist das nicht dasselbe wie "bottom".
"bottom" wäre ein Typ ohne mögliche Werte. Es kann niemals existieren. Wenn eine Funktion "bottom" zurückgibt, kann sie nicht zurückgeben, da es keinen Wert vom Typ "bottom" gibt, den sie zurückgeben könnte.
Wenn ein Sprachdesigner Lust hat, gibt es keinen Grund, diesen Typ nicht zu haben. Die Implementierung ist nicht schwierig (Sie können sie genau wie eine Funktion implementieren, die void zurückgibt und als "don't return" markiert ist). Sie können keine Zeiger auf Funktionen, die bottom zurückgeben, mit Zeigern auf Funktionen, die void zurückgeben, mischen, da sie nicht vom selben Typ sind.
quelle