it-swarm.com.de

Gibt es einen Grund, einen Bottom-Typ in einer Programmiersprache zu haben?

Ein Bodentyp 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 ist.

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 vielleicht löst es eine Ausnahme aus.

Was bringt es, diesen seltsamen Typ in einer Programmiersprache zu haben? Es ist nicht so häufig, aber es ist in einigen vorhanden, wie Scala und LISP.

50
GregRos

Ich nehme ein einfaches Beispiel: C++ vs Rust.

Hier ist eine Funktion, mit der eine Ausnahme in C++ 11 ausgelöst wird:

[[noreturn]] void ThrowException(char const* message,
                                 char const* file,
                                 int line,
                                 char const* function);

Und hier ist das Äquivalent in Rust:

fn formatted_panic(message: &str, file: &str, line: isize, function: &str) -> !;

Aus rein syntaktischen Gründen ist das Konstrukt Rust sinnvoller. Beachten Sie, dass das C++ - Konstrukt ein Rückgabetyp angibt, obwohl es auch angibt, dass es nicht zurückkehren wird. Das ist ein bisschen komisch.

Standardmäßig wurde die C++ - Syntax nur mit C++ 11 angezeigt (sie wurde oben angeheftet), aber verschiedene Compiler stellten seit einiger Zeit verschiedene Erweiterungen bereit, sodass Analysetools von Drittanbietern programmiert werden mussten, um die verschiedenen Möglichkeiten zu erkennen Dieses Attribut könnte geschrieben werden. Eine Standardisierung ist offensichtlich überlegen.


Nun zum Nutzen?

Die Tatsache, dass eine Funktion nicht zurückgegeben wird, kann nützlich sein für:

  • optimierung: Man kann jeden Code danach beschneiden (er wird nicht zurückgegeben), die Register müssen nicht gespeichert werden (da sie nicht wiederhergestellt werden müssen), ...
  • statische Analyse: Es werden eine Reihe potenzieller Ausführungspfade eliminiert
  • wartbarkeit: (siehe statische Analyse, jedoch vom Menschen)
33
Matthieu M.

Karls Antwort ist gut. Hier ist eine zusätzliche Verwendung, die meines Erachtens noch niemand erwähnt hat. Die Art von

if E then A else B

sollte ein Typ sein, der alle Werte im Typ A und alle Werte im Typ B enthält. Wenn der Typ von BNothing ist, kann der Typ des Ausdrucks if der Typ von A sein. Ich werde oft eine Routine erklären

def unreachable( s:String ) : Nothing = throw new AssertionError("Unreachable "+s) 

zu sagen, dass der Code voraussichtlich nicht erreicht wird. Da der Typ Nothing ist, kann unreachable(s) jetzt in jedem if oder (häufiger) switch verwendet werden, ohne den Ergebnistyp zu beeinflussen. Zum Beispiel

 val colour : Colour := switch state of
         BLACK_TO_MOVE: BLACK
         WHITE_TO_MOVE: WHITE
         default: unreachable("Bad state")

Scala hat so einen Nichts-Typ.

Ein weiterer Anwendungsfall für Nothing (wie in Karls Antwort erwähnt) ist List [Nothing]. Dies ist der Listentyp, dessen Mitglieder jeweils den Typ Nothing haben. Somit kann es der Typ der leeren Liste sein.

Die Schlüsseleigenschaft von Nothing, mit der diese Anwendungsfälle funktionieren, ist nicht, dass sie keine Werte hat - obwohl sie in Scala beispielsweise keine Werte hat - es ist, dass sie es ist ist ein Subtyp jedes anderen Typs.

Angenommen, Sie haben eine Sprache, in der jeder Typ denselben Wert enthält - nennen wir ihn (). In einer solchen Sprache könnte der Einheitentyp, dessen einziger Wert () Ist, ein Subtyp jedes Typs sein. Das macht es nicht zu einem Bottom-Typ in dem Sinne, wie es das OP bedeutete; Dem OP war klar, dass ein Bodentyp keine Werte enthält. Da es sich jedoch um einen Typ handelt, der ein Subtyp jedes Typs ist, kann er fast dieselbe 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 forall a.a Haben. Eine Instanz dieses Typschemas wird mit jedem anderen Typ vereinheitlicht, sodass sie effektiv als unterster Typ fungiert, obwohl (Standard-) Haskell keine Vorstellung von Subtypisierung hat. Zum Beispiel hat die Funktion error aus dem Standardvorspiel das Typschema forall a. [Char] -> a. So können Sie schreiben

if E then A else error ""

und der Typ des Ausdrucks ist der gleiche wie der Typ von A für jeden Ausdruck A.

Die leere Liste in Haskell hat das Typschema forall a. [a]. Wenn A ein Ausdruck ist, dessen Typ ein Listentyp ist, dann

if E then A else []

ist ein Ausdruck mit demselben Typ wie A.

27

Typen bilden auf zwei Arten ein Monoid und bilden zusammen ein Semiring . So heißt algebraische Datentypen . Bei endlichen Typen bezieht sich dieses Semiring direkt auf das Semiring natürlicher Zahlen (einschließlich Null). Dies bedeutet, dass Sie zählen, wie viele mögliche Werte der Typ hat (ausgenommen „nicht terminierende Werte“).

  • Der untere Typ (ich nenne ihn Vacuous) hat Nullwerte.
  • Der Einheitentyp hat einen Wert. Ich werde sowohl den Typ als auch seinen Einzelwert () Aufrufen.
  • Die Komposition (die von den meisten Programmiersprachen direkt über Datensätze/Strukturen/Klassen mit öffentlichen Feldern unterstützt wird) ist eine Produktoperation . Zum Beispiel hat (Bool, Bool) Vier mögliche Werte, nämlich (False,False), (False,True), (True,False) Und (True,True).
    Der Einheitentyp ist das Identitätselement der Kompositionsoperation. Z.B. ((), False) Und ((), True) Sind die einzigen Werte vom Typ ((), Bool), Daher ist dieser Typ isomorph zu Bool selbst.
  • Alternative Typen werden in den meisten Sprachen etwas vernachlässigt (OO-Sprachen unterstützen sie irgendwie bei der Vererbung), aber sie sind nicht weniger nützlich. Eine Alternative zwischen zwei Typen A und B hat grundsätzlich alle Werte von A plus alle Werte von B, daher Summentyp . Zum Beispiel hat Either () Bool drei Werte, ich werde sie Left (), Right False Und Right True Nennen.
    Der untere Typ ist das Identitätselement der Summe: Either Vacuous A Hat nur Werte der Form Right a, weil Left ... keinen Sinn ergibt (Vacuous hat 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 ist eine monoidale Kategorie . Auf diese Weise können Sie unter anderem anwendungsbezogene Funktoren und Monaden definieren, die sich als hervorragende Abstraktion für allgemeine Berechnungen (möglicherweise mit Nebenwirkungen usw.) in ansonsten rein funktionalen Begriffen herausstellen.

Eigentlich können Sie ziemlich weit kommen, wenn Sie sich nur um eine Seite des Problems kümmern (das Kompositionsmonoid), dann brauchen Sie den unteren Typ nicht wirklich explizit. Zum Beispiel hatte selbst Haskell lange Zeit keinen Standardbodentyp. Jetzt heißt es Void .

Wenn Sie jedoch das Gesamtbild als bikartesische geschlossene Kategorie betrachten, entspricht das Typensystem tatsächlich dem gesamten Lambda-Kalkül, sodass Sie im Grunde genommen die perfekte Abstraktion über alles Mögliche in einer Turing-vollständigen Sprache haben . Ideal für eingebettete domänenspezifische Sprachen, zum Beispiel gibt es ein Projekt zur direkten Codierung elektronischer Schaltungen auf diese Weise .

Natürlich kann man durchaus sagen, dass dies alles Theoretiker ist allgemeiner Unsinn . Sie müssen überhaupt nichts über Kategorietheorie wissen, um ein guter Programmierer zu sein, aber wenn Sie dies tun, erhalten Sie leistungsstarke und lächerlich allgemeine Möglichkeiten, über Code nachzudenken und Invarianten zu beweisen.


mb21 erinnert mich daran, dass dies nicht mit unteren Werten verwechselt werden sollte. In faulen Sprachen wie Haskell enthält jeder Typ einen unteren „Wert“ mit der Bezeichnung . 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 wiederholt wird. Sogar Haskells Void Typ "enthält" den unteren Wert, also den Namen. In diesem Licht hat Haskells unterer Typ wirklich einen Wert und sein Einheitentyp zwei Werte, aber in der kategorietheoretischen Diskussion wird dies im Allgemeinen ignoriert.

19
leftaroundabout

Vielleicht schleift es für immer, oder vielleicht löst es eine Ausnahme aus.

Klingt nach einem nützlichen Typ in solchen Situationen, auch wenn er selten ist.

Auch wenn Nothing (Scalas Name für den unteren Typ) keine Werte haben kann, List[Nothing] hat diese Einschränkung nicht, was es als Typ einer leeren Liste nützlich macht. Die meisten Sprachen umgehen dies, indem sie eine leere Liste von Zeichenfolgen von einem anderen Typ als eine leere Liste von Ganzzahlen machen. Dies ist zwar sinnvoll, macht das Schreiben einer leeren Liste jedoch ausführlicher, was in einer listenorientierten Sprache ein großer Nachteil ist.

18
Karl Bielefeldt

Für die statische Analyse ist es hilfreich, die Tatsache zu dokumentieren, dass ein bestimmter Codepfad nicht erreichbar ist. Wenn Sie beispielsweise Folgendes in C # schreiben:

int F(int arg) {
 if (arg != 0)
  return arg + 1; //some computation
 else
  Assert(false); //this throws but the compiler does not know that
}
void Assert(bool cond) { if (!cond) throw ...; }

Der Compiler beschwert sich, dass F in mindestens einem Codepfad nichts zurückgibt. Wenn Assert als nicht zurückgegeben markiert würde, müsste der Compiler nicht warnen.

3
usr

In einigen Sprachen hat null den unteren Typ, da der Subtyp aller Typen genau definiert, wofür Sprachen null verwenden (trotz des leichten Widerspruchs, dass null sowohl sich selbst als auch eine Funktion ist, die sich selbst zurückgibt unter Vermeidung der allgemeinen Argumente, warum bot unbewohnt sein sollte).

Es kann auch als Sammelbegriff für Funktionstypen verwendet werden (any -> bot) um den Versand schief zu handhaben.

In einigen Sprachen können Sie bot tatsächlich als Fehler auflösen, um benutzerdefinierte Compilerfehler bereitzustellen.

2
Telastyn

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 nützlich zu wissen, aber am Ende können Sie eine Funktion wie diese wie jede andere aufrufen. Der Compiler kann die Informationen zur Optimierung verwenden, um Warnungen vor totem Code zu geben und so weiter. 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, nimmt dieser Wert null Bits an und benötigt keinen Code. In beiden Fällen ist das nicht dasselbe wie "unten".

"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 dazu 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 "nicht zurückgegeben" markiert ist). Sie können Zeiger auf Funktionen, die unten zurückgegeben werden, nicht mit Zeigern auf Funktionen mischen, die void zurückgeben, da sie nicht vom gleichen Typ sind.

1
gnasher729

Ja, das ist ein sehr nützlicher Typ. Während seine Rolle hauptsächlich innerhalb des Typensystems liegt, gibt es einige Gelegenheiten, in denen der untere Typ offen erscheint.

Stellen Sie sich eine statisch typisierte Sprache vor, in der Bedingungen Ausdrücke sind (daher fungiert die Wenn-Dann-Sonst-Konstruktion auch als ternärer Operator von C und Freunden, und es gibt möglicherweise eine ähnliche Mehrweg-Fallanweisung). 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 (ohne nützlichen Wert zurückzugeben). Im Allgemeinen möchte man, dass jeder Verzweigungsausdruck (implizit) in einen gemeinsamen Typ konvertierbar ist , der der Typ des vollständigen Ausdrucks ist (möglicherweise mit mehr oder weniger) komplizierte Einschränkungen, damit dieser allgemeine Typ vom Complier effektiv gefunden werden kann, 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 bedingter Ausdrücke ermöglicht. Einer ist bereits erwähnt, wobei der Ergebnistyp der Einheitentyp void ist; Dies ist natürlich ein Supertyp aller anderen Typen, und wenn jeder Typ (trivial) in ihn konvertiert werden kann, kann der bedingte Ausdruck als bedingte Anweisung verwendet werden. Der andere betrifft Fälle, in denen der Ausdruck einen nützlichen Wert zurückgibt, ein 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 vom Typ des gesamten Ausdrucks (von einem nicht erreichbaren Punkt aus) erzeugen müssten. Es ist diese Art von Situation, die elegant behandelt werden kann, indem Ausnahmeklauseln, Sprünge und Aufrufe angegeben werden, die einen solchen Effekt haben, der unterste Typ, der eine Typ, der (trivial) in einen anderen Typ konvertiert werden kann.

Ich würde vorschlagen, einen unteren Typ wie * Zu schreiben, um seine Konvertierbarkeit in einen beliebigen Typ vorzuschlagen. Es kann intern anderen nützlichen Zwecken dienen, beispielsweise wenn versucht wird, einen Ergebnistyp für eine rekursive Funktion abzuleiten, die keine deklariert. Der Typ-Inferencer kann jedem rekursiven Aufruf den Typ * Zuweisen, um ein Chicken-and zu vermeiden -Eiersituation; Der tatsächliche Typ wird durch nicht rekursive Zweige bestimmt, und die rekursiven werden in den allgemeinen Typ der nicht rekursiven verzweigt. Wenn es überhaupt keine nicht rekursiven Zweige gibt, bleibt der Typ * Und zeigt korrekt an, dass die Funktion keine Möglichkeit hat jemals von der Rekursion zurückzukehren. Abgesehen davon und als Ergebnisart von Auslösefunktionen kann man * Als Komponententyp von Sequenzen der Länge 0 verwenden, zum Beispiel der leeren Liste; Wenn jemals ein Element aus einem Ausdruck vom Typ [*] (notwendigerweise leere Liste) ausgewählt wird, zeigt der resultierende Typ * korrekt an, dass dies niemals ohne Fehler zurückkehren kann.

1