Types

Typ-Systeme sind traditionell in zwei ganz unterschiedliche Lager gefallen: statische Typ-Systeme, bei denen jeder Programme Ausdruck einen Typ haben muss, der vor der Ausführung des Programms berechnet werden kann, und dynamische Typ-Systeme, bei denen bis zur Laufzeit nichts über Typen bekannt ist, wenn die tatsächlichen Werte, die vom Programm verarbeitet werden, verfügbar sind. Objektorientierung ermöglicht eine gewisse Flexibilität in statisch typisierten Sprachen, indem sie es erlaubt, Code zu schreiben, ohne dass die genauen Typen der Werte zur Compile-Zeit bekannt sind. Die Fähigkeit, Code zu schreiben, der mit verschiedenen Typen arbeiten kann, wird als Polymorphismus bezeichnet. Alle Codes in klassischen dynamisch typisierten Sprachen sind polymorph: Nur durch explizites Überprüfen von Typen oder wenn Objekte zur Laufzeit keine Operationen unterstützen, werden die Typen von Werten jemals eingeschränkt.

Julias Typsystem ist dynamisch, gewinnt jedoch einige der Vorteile statischer Typsysteme, indem es möglich ist, anzugeben, dass bestimmte Werte spezifische Typen haben. Dies kann bei der Generierung effizienter Codes von großem Nutzen sein, aber noch bedeutender ist, dass es die Methodenaufrufe auf die Typen der Funktionsargumente tief in die Sprache integriert. Der Methodenaufruf wird ausführlich in Methods behandelt, ist jedoch im Typsystem verankert, das hier vorgestellt wird.

Das Standardverhalten in Julia, wenn Typen weggelassen werden, erlaubt es, dass Werte jeden Typ haben können. Daher kann man viele nützliche Julia-Funktionen schreiben, ohne jemals explizit Typen zu verwenden. Wenn jedoch zusätzliche Ausdruckskraft benötigt wird, ist es einfach, schrittweise explizite Typannotationen in zuvor "untypisierten" Code einzuführen. Das Hinzufügen von Annotationen dient drei Hauptzwecken: um die leistungsstarke Mehrfachdispatch-Mechanik von Julia zu nutzen, um die Lesbarkeit für Menschen zu verbessern und um Programmierfehler zu erkennen.

Julia in der Sprache von type systems ist: dynamisch, nominativ und parametrisch. Generische Typen können parametrisiert werden, und die hierarchischen Beziehungen zwischen Typen sind explicitly declared, anstatt implied by compatible structure. Ein besonders markantes Merkmal von Julias Typsystem ist, dass konkrete Typen sich nicht gegenseitig untertypen können: Alle konkreten Typen sind final und dürfen nur abstrakte Typen als ihre Supertypen haben. Auch wenn dies zunächst als unangemessen restriktiv erscheinen mag, hat es viele vorteilhafte Konsequenzen mit überraschend wenigen Nachteilen. Es stellt sich heraus, dass die Fähigkeit, Verhalten zu erben, viel wichtiger ist als die Fähigkeit, Struktur zu erben, und das Erben beider verursacht erhebliche Schwierigkeiten in traditionellen objektorientierten Sprachen. Weitere hochrangige Aspekte von Julias Typsystem, die zu Beginn erwähnt werden sollten, sind:

  • Es gibt keine Trennung zwischen Objekt- und Nicht-Objektwerten: Alle Werte in Julia sind wahre Objekte, die einen Typ haben, der zu einem einzigen, vollständig verbundenen Typgraphen gehört, dessen alle Knoten gleichermaßen erstklassig als Typen sind.
  • Es gibt kein sinnvolles Konzept eines "Kompilierungszeit-Typs": Der einzige Typ, den ein Wert hat, ist sein tatsächlicher Typ, wenn das Programm läuft. Dies wird in objektorientierten Sprachen als "Laufzeit-Typ" bezeichnet, wo die Kombination aus statischer Kompilierung und Polymorphismus diese Unterscheidung bedeutend macht.
  • Nur Werte, nicht Variablen, haben Typen – Variablen sind einfach Namen, die an Werte gebunden sind, obwohl wir der Einfachheit halber sagen können "Typ einer Variablen" als Kurzform für "Typ des Wertes, auf den sich eine Variable bezieht".
  • Sowohl abstrakte als auch konkrete Typen können durch andere Typen parametrisiert werden. Sie können auch durch Symbole, durch Werte eines beliebigen Typs, für den isbits wahr zurückgibt (im Wesentlichen Dinge wie Zahlen und Booles, die wie C-Typen oder structs ohne Zeiger auf andere Objekte gespeichert sind), und auch durch Tupel davon. Typparameter können weggelassen werden, wenn sie nicht referenziert oder eingeschränkt werden müssen.

Julias Typsystem ist so gestaltet, dass es leistungsfähig und ausdrucksstark, gleichzeitig jedoch klar, intuitiv und unauffällig ist. Viele Julia-Programmierer werden möglicherweise nie das Bedürfnis verspüren, Code zu schreiben, der explizit Typen verwendet. Einige Arten der Programmierung hingegen werden mit deklarierten Typen klarer, einfacher, schneller und robuster.

Type Declarations

Der :: Operator kann verwendet werden, um Typannotationen an Ausdrücke und Variablen in Programmen anzuhängen. Es gibt zwei Hauptgründe, dies zu tun:

  1. Als eine Behauptung, um zu helfen zu bestätigen, dass Ihr Programm so funktioniert, wie Sie es erwarten, und
  2. Um dem Compiler zusätzliche Typinformationen bereitzustellen, die in einigen Fällen die Leistung verbessern können.

Wenn an einen Ausdruck, der einen Wert berechnet, angehängt, wird der ::-Operator als "ist eine Instanz von" gelesen. Er kann überall verwendet werden, um zu bestätigen, dass der Wert des Ausdrucks auf der linken Seite eine Instanz des Typs auf der rechten Seite ist. Wenn der Typ auf der rechten Seite konkret ist, muss der Wert auf der linken Seite diesen Typ als seine Implementierung haben – denken Sie daran, dass alle konkreten Typen final sind, sodass keine Implementierung ein Subtyp eines anderen ist. Wenn der Typ abstrakt ist, reicht es aus, dass der Wert von einem konkreten Typ implementiert wird, der ein Subtyp des abstrakten Typs ist. Wenn die Typassertion nicht wahr ist, wird eine Ausnahme ausgelöst, andernfalls wird der Wert auf der linken Seite zurückgegeben:

julia> (1+2)::AbstractFloat
ERROR: TypeError: in typeassert, expected AbstractFloat, got a value of type Int64

julia> (1+2)::Int
3

Dies ermöglicht es, eine Typanpassung direkt an jeden Ausdruck anzuhängen.

Wenn es an eine Variable auf der linken Seite einer Zuweisung angehängt wird oder Teil einer local-Deklaration ist, bedeutet der ::-Operator etwas anderes: Er erklärt, dass die Variable immer den angegebenen Typ haben soll, ähnlich einer Typdeklaration in einer statisch typisierten Sprache wie C. Jeder Wert, der der Variable zugewiesen wird, wird in den deklarierten Typ umgewandelt, indem convert verwendet wird:

julia> function foo()
           x::Int8 = 100
           x
       end
foo (generic function with 1 method)

julia> x = foo()
100

julia> typeof(x)
Int8

Dieses Feature ist nützlich, um Leistungs-"Fallen" zu vermeiden, die auftreten könnten, wenn eine der Zuweisungen an eine Variable ihren Typ unerwartet ändert.

Dieses "Deklarations"-Verhalten tritt nur in bestimmten Kontexten auf:

local x::Int8  # in a local declaration
x::Int8 = 10   # as the left-hand side of an assignment

und gilt für den gesamten aktuellen Geltungsbereich, sogar vor der Deklaration.

Seit Julia 1.8 können Typdeklarationen jetzt im globalen Geltungsbereich verwendet werden, d.h. Typannotationen können zu globalen Variablen hinzugefügt werden, um den Zugriff auf sie typstabil zu machen.

julia> x::Int = 10
10

julia> x = 3.5
ERROR: InexactError: Int64(3.5)

julia> function foo(y)
           global x = 15.8    # throws an error when foo is called
           return x + y
       end
foo (generic function with 1 method)

julia> foo(10)
ERROR: InexactError: Int64(15.8)

Deklarationen können auch an Funktionsdefinitionen angehängt werden:

function sinc(x)::Float64
    if x == 0
        return 1
    end
    return sin(pi*x)/(pi*x)
end

Die Rückgabe aus dieser Funktion verhält sich genau wie eine Zuweisung an eine Variable mit einem deklarierten Typ: Der Wert wird immer in Float64 umgewandelt.

Abstract Types

Abstrakte Typen können nicht instanziiert werden und dienen nur als Knoten im Typgraphen, wodurch sie Mengen verwandter konkreter Typen beschreiben: jene konkreten Typen, die ihre Nachkommen sind. Wir beginnen mit abstrakten Typen, obwohl sie keine Instanziierung haben, da sie das Rückgrat des Typsystems bilden: Sie bilden die konzeptionelle Hierarchie, die Julias Typsystem mehr als nur eine Sammlung von Objektimplementierungen macht.

Erinnere dich daran, dass wir in Integers and Floating-Point Numbers eine Vielzahl konkreter Typen von numerischen Werten eingeführt haben: Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float16, Float32, und Float64. Obwohl sie unterschiedliche Darstellungsgrößen haben, haben Int8, Int16, Int32, Int64 und Int128 gemeinsam, dass sie vorzeichenbehaftete Ganzzahltypen sind. Ebenso sind UInt8, UInt16, UInt32, UInt64 und UInt128 alle vorzeichenlosen Ganzzahltypen, während Float16, Float32 und `Float64 sich dadurch unterscheiden, dass sie Fließkommatypen und keine Ganzzahlen sind. Es ist üblich, dass ein Stück Code nur dann sinnvoll ist, wenn seine Argumente eine Art von Ganzzahl sind, aber nicht wirklich davon abhängt, welche Art von Ganzzahl. Zum Beispiel funktioniert der Algorithmus für den größten gemeinsamen Teiler für alle Arten von Ganzzahlen, funktioniert jedoch nicht für Fließkommazahlen. Abstrakte Typen ermöglichen den Aufbau einer Hierarchie von Typen, die einen Kontext bieten, in den konkrete Typen passen können. Dies ermöglicht es Ihnen beispielsweise, einfach für jeden Typ zu programmieren, der eine Ganzzahl ist, ohne einen Algorithmus auf einen bestimmten Typ von Ganzzahl zu beschränken.

Abstrakte Typen werden mit dem abstract type Schlüsselwort deklariert. Die allgemeinen Syntaxen zur Deklaration eines abstrakten Typs sind:

abstract type «name» end
abstract type «name» <: «supertype» end

Das Schlüsselwort abstract type führt einen neuen abstrakten Typ ein, dessen Name durch «name» angegeben wird. Dieser Name kann optional gefolgt werden von <: und einem bereits bestehenden Typ, was anzeigt, dass der neu deklarierte abstrakte Typ ein Untertyp dieses "Eltern"-Typs ist.

Wenn kein Supertyp angegeben ist, ist der Standard-Supertyp Any – ein vordefinierter abstrakter Typ, von dem alle Objekte Instanzen sind und alle Typen Subtypen sind. In der Typentheorie wird Any häufig als "top" bezeichnet, da es sich an der Spitze des Typgraphen befindet. Julia hat auch einen vordefinierten abstrakten "bottom" Typ, der sich am Tiefpunkt des Typgraphen befindet und als Union{} geschrieben wird. Er ist das genaue Gegenteil von Any: Kein Objekt ist eine Instanz von Union{} und alle Typen sind Supertypen von Union{}.

Lassen Sie uns einige der abstrakten Typen betrachten, die die numerische Hierarchie von Julia ausmachen:

abstract type Number end
abstract type Real          <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer       <: Real end
abstract type Signed        <: Integer end
abstract type Unsigned      <: Integer end

Der Number Typ ist ein direkter Kindtyp von Any, und Real ist sein Kind. Im Gegenzug hat Real zwei Kinder (es hat mehr, aber hier werden nur zwei gezeigt; wir werden später auf die anderen eingehen): Integer und AbstractFloat, die die Welt in Darstellungen von Ganzzahlen und Darstellungen von reellen Zahlen unterteilen. Darstellungen von reellen Zahlen umfassen Fließkommatypen, beinhalten aber auch andere Typen, wie z.B. rationale Zahlen. AbstractFloat umfasst nur Fließkommadarstellungen von reellen Zahlen. Ganzzahlen werden weiter unterteilt in Signed und Unsigned Varianten.

Der <: Operator bedeutet allgemein "ist ein Subtyp von" und erklärt, verwendet in Deklarationen wie den oben genannten, den rechten Typ als unmittelbaren Supertyp des neu deklarierten Typs. Er kann auch in Ausdrücken als Subtyp-Operator verwendet werden, der true zurückgibt, wenn sein linker Operand ein Subtyp seines rechten Operanden ist:

julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

Ein wichtiger Einsatz von abstrakten Typen besteht darin, Standardimplementierungen für konkrete Typen bereitzustellen. Um ein einfaches Beispiel zu geben, betrachten Sie:

function myplus(x,y)
    x+y
end

Das erste, was zu beachten ist, ist, dass die obigen Argumentdeklarationen äquivalent zu x::Any und y::Any sind. Wenn diese Funktion aufgerufen wird, sagen wir als myplus(2,5), wählt der Dispatcher die spezifischste Methode mit dem Namen myplus, die zu den gegebenen Argumenten passt. (Siehe Methods für weitere Informationen zu Multiple Dispatch.)

Angenommen, es wird keine spezifischere Methode als die oben genannte gefunden, definiert und kompiliert Julia intern eine Methode namens myplus, die speziell für zwei Int-Argumente basierend auf der oben genannten generischen Funktion ist, d.h. sie definiert und kompiliert implizit:

function myplus(x::Int,y::Int)
    x+y
end

und schließlich ruft es diese spezifische Methode auf.

So ermöglichen abstrakte Typen Programmierern, generische Funktionen zu schreiben, die später als Standardmethode von vielen Kombinationen konkreter Typen verwendet werden können. Dank des mehrfachen Dispatch hat der Programmierer die volle Kontrolle darüber, ob die Standard- oder die spezifischere Methode verwendet wird.

Ein wichtiger Punkt ist, dass es keinen Leistungsverlust gibt, wenn der Programmierer sich auf eine Funktion verlässt, deren Argumente abstrakte Typen sind, da sie für jedes Tupel konkreter Argumenttypen, mit denen sie aufgerufen wird, neu kompiliert wird. (Es kann jedoch ein Leistungsproblem auftreten, wenn es sich um Funktionsargumente handelt, die Container abstrakter Typen sind; siehe Performance Tips.)

Primitive Types

Warning

Es ist fast immer vorzuziehen, einen bestehenden primitiven Typ in einen neuen zusammengesetzten Typ zu wickeln, als einen eigenen primitiven Typ zu definieren.

Diese Funktionalität existiert, um Julia zu ermöglichen, die standardmäßigen primitiven Typen, die LLVM unterstützt, zu bootstrappen. Sobald sie definiert sind, gibt es nur sehr wenig Grund, weitere zu definieren.

Ein primitiver Typ ist ein konkreter Typ, dessen Daten aus einfachen alten Bits bestehen. Klassische Beispiele für primitive Typen sind Ganzzahlen und Gleitkommawerte. Im Gegensatz zu den meisten Sprachen erlaubt es Julia, eigene primitive Typen zu deklarieren, anstatt nur eine feste Menge an eingebauten Typen bereitzustellen. Tatsächlich sind die standardmäßigen primitiven Typen alle in der Sprache selbst definiert:

primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end

primitive type Bool <: Integer 8 end
primitive type Char <: AbstractChar 32 end

primitive type Int8    <: Signed   8 end
primitive type UInt8   <: Unsigned 8 end
primitive type Int16   <: Signed   16 end
primitive type UInt16  <: Unsigned 16 end
primitive type Int32   <: Signed   32 end
primitive type UInt32  <: Unsigned 32 end
primitive type Int64   <: Signed   64 end
primitive type UInt64  <: Unsigned 64 end
primitive type Int128  <: Signed   128 end
primitive type UInt128 <: Unsigned 128 end

Die allgemeinen Syntaxen zur Deklaration eines primitiven Typs sind:

primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end

Die Anzahl der Bits gibt an, wie viel Speicher der Typ benötigt, und der Name verleiht dem neuen Typ einen Namen. Ein primitiver Typ kann optional als Untertyp eines bestimmten Supertyps deklariert werden. Wenn ein Supertyp weggelassen wird, hat der Typ standardmäßig Any als seinen unmittelbaren Supertyp. Die Deklaration von Bool oben bedeutet daher, dass ein boolescher Wert acht Bits zum Speichern benötigt und Integer als seinen unmittelbaren Supertyp hat. Derzeit werden nur Größen unterstützt, die Vielfache von 8 Bits sind, und Sie werden wahrscheinlich auf LLVM-Fehler mit Größen stoßen, die von den oben verwendeten abweichen. Daher können boolesche Werte, obwohl sie tatsächlich nur ein einzelnes Bit benötigen, nicht kleiner als acht Bits deklariert werden.

Die Typen Bool, Int8 und UInt8 haben alle identische Darstellungen: Sie sind acht-Bit-Chunks des Speichers. Da Julias Typsystem jedoch nominativ ist, sind sie trotz identischer Struktur nicht austauschbar. Ein grundlegender Unterschied zwischen ihnen ist, dass sie unterschiedliche Supertypen haben: Der direkte Supertyp von 4d61726b646f776e2e436f64652822222c2022426f6f6c2229_40726566 ist Integer, der von 4d61726b646f776e2e436f64652822222c2022496e74382229_40726566 ist Signed, und der von 4d61726b646f776e2e436f64652822222c202255496e74382229_40726566 ist Unsigned. Alle anderen Unterschiede zwischen 4d61726b646f776e2e436f64652822222c2022426f6f6c2229_40726566, 4d61726b646f776e2e436f64652822222c2022496e74382229_40726566 und 4d61726b646f776e2e436f64652822222c202255496e74382229_40726566 sind Verhaltensfragen – die Art und Weise, wie Funktionen definiert sind, um zu handeln, wenn ihnen Objekte dieser Typen als Argumente übergeben werden. Deshalb ist ein nominatives Typsystem notwendig: Wenn die Struktur den Typ bestimmen würde, der wiederum das Verhalten diktiert, wäre es unmöglich, 4d61726b646f776e2e436f64652822222c2022426f6f6c2229_40726566 anders zu verhalten als 4d61726b646f776e2e436f64652822222c2022496e74382229_40726566 oder 4d61726b646f776e2e436f64652822222c202255496e74382229_40726566.

Composite Types

Composite types werden in verschiedenen Sprachen als Datensätze, Strukturen oder Objekte bezeichnet. Ein zusammengesetzter Typ ist eine Sammlung von benannten Feldern, deren Instanz als ein einzelner Wert behandelt werden kann. In vielen Sprachen sind zusammengesetzte Typen die einzige Art von benutzerdefiniertem Typ, und sie sind bei weitem der am häufigsten verwendete benutzerdefinierte Typ in Julia.

In mainstream objektorientierten Sprachen wie C++, Java, Python und Ruby haben zusammengesetzte Typen auch benannte Funktionen, die mit ihnen verbunden sind, und die Kombination wird als "Objekt" bezeichnet. In reineren objektorientierten Sprachen wie Ruby oder Smalltalk sind alle Werte Objekte, unabhängig davon, ob sie zusammengesetzt sind oder nicht. In weniger reinen objektorientierten Sprachen, einschließlich C++ und Java, sind einige Werte, wie Ganzzahlen und Gleitkommawerte, keine Objekte, während Instanzen benutzerdefinierter zusammengesetzter Typen wahre Objekte mit zugehörigen Methoden sind. In Julia sind alle Werte Objekte, aber Funktionen sind nicht mit den Objekten gebündelt, auf denen sie operieren. Dies ist notwendig, da Julia auswählt, welche Methode einer Funktion verwendet werden soll, durch Mehrfachdispatch, was bedeutet, dass die Typen aller Argumente einer Funktion berücksichtigt werden, wenn eine Methode ausgewählt wird, und nicht nur das erste (siehe Methods für weitere Informationen zu Methoden und Dispatch). Daher wäre es unangemessen, dass Funktionen nur ihrem ersten Argument "gehören". Die Organisation von Methoden in Funktionsobjekten, anstatt benannte Sammlungen von Methoden "innerhalb" jedes Objekts zu haben, erweist sich als ein äußerst vorteilhafter Aspekt des Sprachdesigns.

Zusammengesetzte Typen werden mit dem struct Schlüsselwort eingeführt, gefolgt von einem Block von Feldnamen, die optional mit Typen unter Verwendung des :: Operators annotiert sind:

julia> struct Foo
           bar
           baz::Int
           qux::Float64
       end

Felder ohne Typannotation haben standardmäßig den Typ Any und können entsprechend jeden Wert halten.

Neue Objekte vom Typ Foo werden erstellt, indem das Foo-Typobjekt wie eine Funktion auf Werte für seine Felder angewendet wird:

julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)

julia> typeof(foo)
Foo

Wenn ein Typ wie eine Funktion angewendet wird, wird dies als Konstruktor bezeichnet. Zwei Konstruktoren werden automatisch generiert (diese werden als Standardkonstruktoren bezeichnet). Der eine akzeptiert beliebige Argumente und ruft convert auf, um sie in die Typen der Felder zu konvertieren, und der andere akzeptiert Argumente, die genau mit den Feldtypen übereinstimmen. Der Grund, warum beide generiert werden, ist, dass dies das Hinzufügen neuer Definitionen erleichtert, ohne versehentlich einen Standardkonstruktor zu ersetzen.

Da das Feld bar in seinem Typ nicht eingeschränkt ist, ist jeder Wert zulässig. Der Wert für baz muss jedoch in Int umwandelbar sein:

julia> Foo((), 23.5, 1)
ERROR: InexactError: Int64(23.5)
Stacktrace:
[...]

Sie können eine Liste von Feldnamen mit der Funktion fieldnames finden.

julia> fieldnames(Foo)
(:bar, :baz, :qux)

Sie können auf die Feldwerte eines zusammengesetzten Objekts mit der traditionellen foo.bar-Notation zugreifen:

julia> foo.bar
"Hello, world."

julia> foo.baz
23

julia> foo.qux
1.5

Zusammengesetzte Objekte, die mit struct deklariert werden, sind unveränderlich; sie können nach der Konstruktion nicht mehr geändert werden. Das mag zunächst seltsam erscheinen, hat jedoch mehrere Vorteile:

  • Es kann effizienter sein. Einige Strukturen können effizient in Arrays gepackt werden, und in einigen Fällen ist der Compiler in der Lage, die Zuweisung unveränderlicher Objekte vollständig zu vermeiden.
  • Es ist nicht möglich, die Invarianten, die von den Konstruktoren des Typs bereitgestellt werden, zu verletzen.
  • Code mit unveränderlichen Objekten kann einfacher zu verstehen sein.

Ein unveränderliches Objekt kann veränderliche Objekte, wie Arrays, als Felder enthalten. Diese enthaltenen Objekte bleiben veränderlich; nur die Felder des unveränderlichen Objekts selbst können nicht geändert werden, um auf andere Objekte zu verweisen.

Wo erforderlich können veränderbare zusammengesetzte Objekte mit dem Schlüsselwort mutable struct deklariert werden, das im nächsten Abschnitt besprochen wird.

Wenn alle Felder einer unveränderlichen Struktur ununterscheidbar sind (===), dann sind auch zwei unveränderliche Werte, die diese Felder enthalten, ununterscheidbar:

julia> struct X
           a::Int
           b::Float64
       end

julia> X(1, 2) === X(1, 2)
true

Es gibt viel mehr zu sagen darüber, wie Instanzen von zusammengesetzten Typen erstellt werden, aber diese Diskussion hängt sowohl von Parametric Types als auch von Methods ab und ist ausreichend wichtig, um in einem eigenen Abschnitt behandelt zu werden: Constructors.

Für viele benutzerdefinierte Typen X möchten Sie möglicherweise eine Methode Base.broadcastable(x::X) = Ref(x) definieren, damit Instanzen dieses Typs als 0-dimensionale "Skalare" für broadcasting fungieren.

Mutable Composite Types

Wenn ein zusammengesetzter Typ mit mutable struct anstelle von struct deklariert wird, können Instanzen davon geändert werden:

julia> mutable struct Bar
           baz
           qux::Float64
       end

julia> bar = Bar("Hello", 1.5);

julia> bar.qux = 2.0
2.0

julia> bar.baz = 1//2
1//2

Eine zusätzliche Schnittstelle zwischen den Feldern und dem Benutzer kann durch Instance Properties bereitgestellt werden. Dies gewährt mehr Kontrolle darüber, was mit der bar.baz-Notation zugegriffen und geändert werden kann.

Um Mutation zu unterstützen, werden solche Objekte in der Regel im Heap zugewiesen und haben stabile Speicheradressen. Ein veränderliches Objekt ist wie ein kleiner Behälter, der im Laufe der Zeit unterschiedliche Werte halten kann, und kann daher nur zuverlässig mit seiner Adresse identifiziert werden. Im Gegensatz dazu ist eine Instanz eines unveränderlichen Typs mit bestimmten Feldwerten verbunden – die Feldwerte allein sagen Ihnen alles über das Objekt. Bei der Entscheidung, ob ein Typ veränderlich sein soll, fragen Sie sich, ob zwei Instanzen mit denselben Feldwerten als identisch betrachtet werden würden oder ob sie im Laufe der Zeit unabhängig voneinander geändert werden müssten. Wenn sie als identisch betrachtet würden, sollte der Typ wahrscheinlich unveränderlich sein.

Um zusammenzufassen, zwei wesentliche Eigenschaften definieren Unveränderlichkeit in Julia:

  • Es ist nicht erlaubt, den Wert eines unveränderlichen Typs zu ändern.
    • Für Bittypen bedeutet dies, dass das Bitmuster eines Wertes, sobald es festgelegt ist, sich niemals ändert und dieser Wert die Identität eines Bittyps ist.
    • Für zusammengesetzte Typen bedeutet dies, dass die Identität der Werte ihrer Felder sich niemals ändern wird. Wenn die Felder Bit-Typen sind, bedeutet das, dass sich ihre Bits niemals ändern werden. Für Felder, deren Werte veränderliche Typen wie Arrays sind, bedeutet das, dass die Felder immer auf denselben veränderlichen Wert verweisen, auch wenn der Inhalt dieses veränderlichen Wertes selbst geändert werden kann.
  • Ein Objekt mit einem unveränderlichen Typ kann vom Compiler frei kopiert werden, da seine Unveränderlichkeit es unmöglich macht, programmgesteuert zwischen dem ursprünglichen Objekt und einer Kopie zu unterscheiden.
    • Insbesondere bedeutet dies, dass kleine, unveränderliche Werte wie Ganzzahlen und Fließkommazahlen typischerweise in Registern (oder im Stack) an Funktionen übergeben werden.
    • Veränderliche Werte hingegen werden im Heap zugewiesen und als Zeiger auf im Heap zugewiesene Werte an Funktionen übergeben, es sei denn, der Compiler ist sich sicher, dass es keine Möglichkeit gibt, zu erkennen, dass dies nicht der Fall ist.

In Fällen, in denen ein oder mehrere Felder einer ansonsten veränderbaren Struktur als unveränderlich bekannt sind, kann man diese Felder mit const wie unten gezeigt deklarieren. Dies ermöglicht einige, aber nicht alle Optimierungen von unveränderlichen Strukturen und kann verwendet werden, um Invarianten für die bestimmten als const markierten Felder durchzusetzen.

Julia 1.8

const annotierende Felder von veränderlichen Strukturen erfordert mindestens Julia 1.8.

julia> mutable struct Baz
           a::Int
           const b::Float64
       end

julia> baz = Baz(1, 1.5);

julia> baz.a = 2
2

julia> baz.b = 2.0
ERROR: setfield!: const field .b of type Baz cannot be changed
[...]

Declared Types

Die drei Arten von Typen (abstrakt, primitiv, zusammengesetzt), die in den vorherigen Abschnitten besprochen wurden, sind tatsächlich alle eng miteinander verwandt. Sie teilen sich die gleichen Schlüsselmerkmale:

  • Sie sind ausdrücklich erklärt.
  • Sie haben Namen.
  • Sie haben explizit Supertypen deklariert.
  • Sie können Parameter haben.

Aufgrund dieser gemeinsamen Eigenschaften werden diese Typen intern als Instanzen desselben Konzepts, DataType, dargestellt, das der Typ eines dieser Typen ist:

julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

Ein DataType kann abstrakt oder konkret sein. Wenn er konkret ist, hat er eine festgelegte Größe, ein Speicherlayout und (optional) Feldnamen. Somit ist ein primitiver Typ ein DataType mit einer Größe ungleich null, aber ohne Feldnamen. Ein zusammengesetzter Typ ist ein DataType, der Feldnamen hat oder leer ist (Größe null).

Jeder konkrete Wert im System ist eine Instanz eines bestimmten DataType.

Type Unions

Ein Typenverein ist ein spezieller abstrakter Typ, der als Objekte alle Instanzen seiner Argumenttypen umfasst, die mit dem speziellen Union Schlüsselwort konstruiert werden:

julia> IntOrString = Union{Int,AbstractString}
Union{Int64, AbstractString}

julia> 1 :: IntOrString
1

julia> "Hello!" :: IntOrString
"Hello!"

julia> 1.0 :: IntOrString
ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got a value of type Float64

Die Compiler für viele Sprachen haben eine interne Union-Konstruktion zur Typenbeurteilung; Julia stellt sie einfach dem Programmierer zur Verfügung. Der Julia-Compiler ist in der Lage, effizienten Code im Beisein von Union-Typen mit einer kleinen Anzahl von Typen [1] zu generieren, indem er spezialisierten Code in separaten Zweigen für jeden möglichen Typ erzeugt.

Ein besonders nützlicher Fall eines Union-Typs ist Union{T, Nothing}, wobei T jeder Typ sein kann und Nothing der Singleton-Typ ist, dessen einzige Instanz das Objekt nothing ist. Dieses Muster ist das Äquivalent von Nullable, Option or Maybe-Typen in anderen Sprachen. Das Deklarieren eines Funktionsarguments oder eines Feldes als Union{T, Nothing} ermöglicht es, es entweder auf einen Wert des Typs T oder auf nothing zu setzen, um anzuzeigen, dass kein Wert vorhanden ist. Siehe this FAQ entry für weitere Informationen.

Parametric Types

Ein wichtiges und leistungsstarkes Merkmal von Julias Typsystem ist, dass es parametrisch ist: Typen können Parameter annehmen, sodass Typdeklarationen tatsächlich eine ganze Familie neuer Typen einführen – einen für jede mögliche Kombination von Parameterwerten. Es gibt viele Sprachen, die eine Version von generic programming unterstützen, in denen Datenstrukturen und Algorithmen zu deren Manipulation spezifiziert werden können, ohne die genauen beteiligten Typen anzugeben. Zum Beispiel existiert eine Form der generischen Programmierung in ML, Haskell, Ada, Eiffel, C++, Java, C#, F# und Scala, um nur einige zu nennen. Einige dieser Sprachen unterstützen echten parametrischen Polymorphismus (z. B. ML, Haskell, Scala), während andere ad-hoc, template-basierte Stile der generischen Programmierung unterstützen (z. B. C++, Java). Angesichts der vielen verschiedenen Varianten der generischen Programmierung und parametrischen Typen in verschiedenen Sprachen werden wir nicht einmal versuchen, Julias parametrische Typen mit anderen Sprachen zu vergleichen, sondern uns stattdessen darauf konzentrieren, Julias System für sich selbst zu erklären. Wir werden jedoch anmerken, dass, da Julia eine dynamisch typisierte Sprache ist und nicht alle Typentscheidungen zur Compile-Zeit treffen muss, viele traditionelle Schwierigkeiten, die in statischen parametrischen Typsystemen auftreten, relativ einfach gehandhabt werden können.

Alle deklarierten Typen (der DataType-Variante) können parametrisiert werden, und zwar mit derselben Syntax in jedem Fall. Wir werden sie in folgender Reihenfolge besprechen: zuerst parametrische zusammengesetzte Typen, dann parametrische abstrakte Typen und schließlich parametrische primitive Typen.

Parametric Composite Types

Typ-Parameter werden unmittelbar nach dem Typnamen eingeführt, umgeben von geschweiften Klammern:

julia> struct Point{T}
           x::T
           y::T
       end

Diese Deklaration definiert einen neuen parametrischen Typ, Point{T}, der zwei "Koordinaten" vom Typ T hält. Was, so könnte man fragen, ist T? Nun, das ist genau der Punkt parametrischer Typen: Es kann jeder Typ sein (oder ein Wert eines beliebigen Bittyps, tatsächlich, obwohl er hier eindeutig als Typ verwendet wird). Point{Float64} ist ein konkreter Typ, der dem Typ entspricht, der durch das Ersetzen von T in der Definition von Point mit Float64 definiert wird. Somit erklärt diese einzelne Deklaration tatsächlich eine unbegrenzte Anzahl von Typen: Point{Float64}, Point{AbstractString}, Point{Int64}, usw. Jeder dieser Typen ist jetzt ein verwendbarer konkreter Typ:

julia> Point{Float64}
Point{Float64}

julia> Point{AbstractString}
Point{AbstractString}

Der Typ Point{Float64} ist ein Punkt, dessen Koordinaten 64-Bit-Gleitkommawerte sind, während der Typ Point{AbstractString} ein "Punkt" ist, dessen "Koordinaten" String-Objekte sind (siehe Strings).

Point selbst ist auch ein gültiger Typobjekt, das alle Instanzen Point{Float64}, Point{AbstractString}, usw. als Subtypen enthält:

julia> Point{Float64} <: Point
true

julia> Point{AbstractString} <: Point
true

Andere Typen sind natürlich keine Untertypen davon:

julia> Float64 <: Point
false

julia> AbstractString <: Point
false

Konkrete Point-Typen mit unterschiedlichen Werten von T sind niemals Subtypen voneinander:

julia> Point{Float64} <: Point{Int64}
false

julia> Point{Float64} <: Point{Real}
false
Warning

Dieser letzte Punkt ist sehr wichtig: auch wenn Float64 <: Real haben wir NICHT Point{Float64} <: Point{Real}.

Mit anderen Worten, in der Sprache der Typentheorie sind Julias Typparameter invariant, anstatt covariant (or even contravariant) zu sein. Dies hat praktische Gründe: Während jede Instanz von Point{Float64} konzeptionell wie eine Instanz von Point{Real} sein kann, haben die beiden Typen unterschiedliche Darstellungen im Speicher:

  • Eine Instanz von Point{Float64} kann kompakt und effizient als ein unmittelbares Paar von 64-Bit-Werten dargestellt werden;
  • Eine Instanz von Point{Real} muss in der Lage sein, jedes Paar von Instanzen von Real zu halten. Da Objekte, die Instanzen von Real sind, beliebige Größen und Strukturen haben können, muss eine Instanz von Point{Real} in der Praxis als ein Paar von Zeigern auf individuell zugewiesene Real-Objekte dargestellt werden.

Die Effizienz, die durch die Möglichkeit gewonnen wird, Point{Float64}-Objekte mit unmittelbaren Werten zu speichern, wird im Fall von Arrays enorm verstärkt: Ein Array{Float64} kann als zusammenhängender Speicherblock von 64-Bit-Gleitkommawerten gespeichert werden, während ein Array{Real} ein Array von Zeigern auf individuell zugewiesene Real-Objekte sein muss – die möglicherweise boxed 64-Bit-Gleitkommawerte sind, aber auch beliebig große, komplexe Objekte sein könnten, die als Implementierungen des abstrakten Typs Real deklariert sind.

Da Point{Float64} kein Subtyp von Point{Real} ist, kann die folgende Methode nicht auf Argumente vom Typ Point{Float64} angewendet werden:

function norm(p::Point{Real})
    sqrt(p.x^2 + p.y^2)
end

Eine korrekte Möglichkeit, eine Methode zu definieren, die alle Argumente vom Typ Point{T} akzeptiert, wobei T ein Untertyp von Real ist, lautet:

function norm(p::Point{<:Real})
    sqrt(p.x^2 + p.y^2)
end

(Äquivalent könnte man function norm(p::Point{T} where T<:Real) oder function norm(p::Point{T}) where T<:Real definieren; siehe UnionAll Types.)

Weitere Beispiele werden später in Methods besprochen.

Wie konstruiert man ein Point-Objekt? Es ist möglich, benutzerdefinierte Konstruktoren für zusammengesetzte Typen zu definieren, die ausführlich in Constructors behandelt werden, aber in Abwesenheit von speziellen Konstruktorerklärungen gibt es zwei Standardmethoden zur Erstellung neuer zusammengesetzter Objekte: eine, bei der die Typparameter explizit angegeben werden, und die andere, bei der sie durch die Argumente des Objektkonstruktors impliziert werden.

Da der Typ Point{Float64} ein konkreter Typ ist, der äquivalent zu Point ist, der mit Float64 anstelle von T deklariert wurde, kann er entsprechend als Konstruktor angewendet werden:

julia> p = Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(p)
Point{Float64}

Für den Standardkonstruktor muss genau ein Argument für jedes Feld bereitgestellt werden:

julia> Point{Float64}(1.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64)
The type `Point{Float64}` exists, but no method is defined for this combination of argument types when trying to construct it.
[...]

julia> Point{Float64}(1.0, 2.0, 3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
The type `Point{Float64}` exists, but no method is defined for this combination of argument types when trying to construct it.
[...]

Nur ein Standardkonstruktor wird für parametrische Typen generiert, da es nicht möglich ist, ihn zu überschreiben. Dieser Konstruktor akzeptiert beliebige Argumente und konvertiert sie in die Feldtypen.

In vielen Fällen ist es redundant, den Typ des Point-Objekts anzugeben, das man konstruieren möchte, da die Typen der Argumente des Konstruktors bereits implizit Typinformationen bereitstellen. Aus diesem Grund kann man auch Point selbst als Konstruktor verwenden, vorausgesetzt, der implizierte Wert des Parameter-Typs T ist eindeutig:

julia> p1 = Point(1.0,2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(p1)
Point{Float64}

julia> p2 = Point(1,2)
Point{Int64}(1, 2)

julia> typeof(p2)
Point{Int64}

Im Falle von Point ist der Typ von T eindeutig impliziert, wenn und nur wenn die beiden Argumente von Point denselben Typ haben. Wenn dies nicht der Fall ist, schlägt der Konstruktor mit einem MethodError fehl:

julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.

Closest candidates are:
  Point(::T, !Matched::T) where T
   @ Main none:2

Stacktrace:
[...]

Konstruktor-Methoden, um solche Mischfälle angemessen zu behandeln, können definiert werden, aber das wird erst später in Constructors besprochen.

Parametric Abstract Types

Parametrische abstrakte Typdeklarationen erklären eine Sammlung von abstrakten Typen, ähnlich wie:

julia> abstract type Pointy{T} end

Mit dieser Deklaration ist Pointy{T} ein distinct abstrakter Typ für jeden Typ oder ganzzahligen Wert von T. Wie bei parametrischen zusammengesetzten Typen ist jede solche Instanz ein Subtyp von Pointy:

julia> Pointy{Int64} <: Pointy
true

julia> Pointy{1} <: Pointy
true

Parametrische abstrakte Typen sind invariant, ebenso wie parametrische zusammengesetzte Typen:

julia> Pointy{Float64} <: Pointy{Real}
false

julia> Pointy{Real} <: Pointy{Float64}
false

Die Notation Pointy{<:Real} kann verwendet werden, um das Julia-Äquivalent eines kovarianten Typs auszudrücken, während Pointy{>:Int} das Äquivalent eines kontravarianten Typs darstellt, aber technisch gesehen repräsentieren diese Mengen von Typen (siehe UnionAll Types).

julia> Pointy{Float64} <: Pointy{<:Real}
true

julia> Pointy{Real} <: Pointy{>:Int}
true

So wie einfache abstrakte Typen dazu dienen, eine nützliche Hierarchie von Typen über konkreten Typen zu schaffen, dienen parametrische abstrakte Typen demselben Zweck in Bezug auf parametrische zusammengesetzte Typen. Wir könnten zum Beispiel Point{T} als Untertyp von Pointy{T} wie folgt deklariert haben:

julia> struct Point{T} <: Pointy{T}
           x::T
           y::T
       end

Gegeben eine solche Deklaration haben wir für jede Wahl von T Point{T} als einen Subtyp von Pointy{T}:

julia> Point{Float64} <: Pointy{Float64}
true

julia> Point{Real} <: Pointy{Real}
true

julia> Point{AbstractString} <: Pointy{AbstractString}
true

Diese Beziehung ist ebenfalls invariant:

julia> Point{Float64} <: Pointy{Real}
false

julia> Point{Float64} <: Pointy{<:Real}
true

Parametrische abstrakte Typen wie Pointy dienen dazu, eine flexible und wiederverwendbare Schnittstelle für verschiedene Implementierungen von Punkten zu schaffen. Sie ermöglichen es, verschiedene Punktarten zu definieren, die unterschiedliche Eigenschaften oder Verhaltensweisen haben, während sie dennoch eine gemeinsame Basis teilen.

julia> struct DiagPoint{T} <: Pointy{T}
           x::T
       end

Jetzt sind sowohl Point{Float64} als auch DiagPoint{Float64} Implementierungen der Abstraktion Pointy{Float64}, und ähnlich für jede andere mögliche Wahl des Typs T. Dies ermöglicht das Programmieren über eine gemeinsame Schnittstelle, die von allen Pointy-Objekten geteilt wird, die sowohl für Point als auch für DiagPoint implementiert ist. Dies kann jedoch nicht vollständig demonstriert werden, bis wir Methoden und Dispatch im nächsten Abschnitt, Methods, eingeführt haben.

Es gibt Situationen, in denen es möglicherweise keinen Sinn macht, dass Typparameter frei über alle möglichen Typen variieren. In solchen Situationen kann man den Bereich von T wie folgt einschränken:

julia> abstract type Pointy{T<:Real} end

Mit einer solchen Deklaration ist es akzeptabel, jeden Typ zu verwenden, der ein Subtyp von Real anstelle von T ist, jedoch keine Typen, die keine Subtypen von Real sind:

julia> Pointy{Float64}
Pointy{Float64}

julia> Pointy{Real}
Pointy{Real}

julia> Pointy{AbstractString}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got Type{AbstractString}

julia> Pointy{1}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got a value of type Int64

Typparameter für parametrische zusammengesetzte Typen können auf die gleiche Weise eingeschränkt werden:

struct Point{T<:Real} <: Pointy{T}
    x::T
    y::T
end

Um ein reales Beispiel dafür zu geben, wie all diese parametrischen Typen nützlich sein können, hier ist die tatsächliche Definition von Julias Rational unveränderlichem Typ (außer dass wir den Konstruktor hier der Einfachheit halber weglassen), der ein genaues Verhältnis von Ganzzahlen darstellt:

struct Rational{T<:Integer} <: Real
    num::T
    den::T
end

Es macht nur Sinn, Verhältnisse von Ganzzahlen zu bilden, daher ist der Parameter-Typ T auf einen Untertyp von Integer beschränkt, und ein Verhältnis von Ganzzahlen stellt einen Wert auf der reellen Zahlengeraden dar, sodass jede Rational eine Instanz der Real Abstraktion ist.

Tuple Types

Tupel sind eine Abstraktion der Argumente einer Funktion – ohne die Funktion selbst. Die wesentlichen Aspekte der Argumente einer Funktion sind ihre Reihenfolge und ihre Typen. Daher ähnelt ein Tupeltyp einem parametrisierten unveränderlichen Typ, bei dem jeder Parameter der Typ eines Feldes ist. Zum Beispiel ähnelt ein Tupeltyp mit 2 Elementen dem folgenden unveränderlichen Typ:

struct Tuple2{A,B}
    a::A
    b::B
end

Es gibt jedoch drei wesentliche Unterschiede:

  • Tuple-Typen können beliebig viele Parameter haben.
  • Tuple-Typen sind kovariant in ihren Parametern: Tuple{Int} ist ein Subtyp von Tuple{Any}. Daher wird Tuple{Any} als abstrakter Typ betrachtet, und Tuple-Typen sind nur konkret, wenn ihre Parameter es sind.
  • Tupel haben keine Feldnamen; Felder werden nur über den Index zugegriffen.

Tupelwerte werden mit Klammern und Kommas geschrieben. Wenn ein Tupel erstellt wird, wird bei Bedarf ein entsprechender Tupeltyp generiert:

julia> typeof((1,"foo",2.5))
Tuple{Int64, String, Float64}

Beachten Sie die Implikationen der Kovarianz:

julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true

julia> Tuple{Int,AbstractString} <: Tuple{Real,Real}
false

julia> Tuple{Int,AbstractString} <: Tuple{Real,}
false

Intuitiv bedeutet dies, dass der Typ der Argumente einer Funktion ein Subtyp der Signatur der Funktion ist (wenn die Signatur übereinstimmt).

Vararg Tuple Types

Der letzte Parameter eines Tupeltyps kann der spezielle Wert Vararg sein, der eine beliebige Anzahl von nachfolgenden Elementen bezeichnet:

julia> mytupletype = Tuple{AbstractString,Vararg{Int}}
Tuple{AbstractString, Vararg{Int64}}

julia> isa(("1",), mytupletype)
true

julia> isa(("1",1), mytupletype)
true

julia> isa(("1",1,2), mytupletype)
true

julia> isa(("1",1,2,3.0), mytupletype)
false

Darüber hinaus entspricht Vararg{T} null oder mehr Elementen des Typs T. Vararg-Tupeltypen werden verwendet, um die Argumente darzustellen, die von Varargs-Methoden akzeptiert werden (siehe Varargs Functions).

Der spezielle Wert Vararg{T,N} (wenn er als letzter Parameter eines Tupeltyps verwendet wird) entspricht genau N Elementen des Typs T. NTuple{N,T} ist ein praktischer Alias für Tuple{Vararg{T,N}}, d.h. ein Tupeltyp, der genau N Elemente des Typs T enthält.

Named Tuple Types

Benannte Tupel sind Instanzen des NamedTuple Typs, der zwei Parameter hat: ein Tupel von Symbolen, das die Feldnamen angibt, und einen Tupeltyp, der die Feldtypen angibt. Zur Vereinfachung werden NamedTuple-Typen mit dem @NamedTuple Makro ausgegeben, das eine bequeme struct-ähnliche Syntax für die Deklaration dieser Typen über key::Type-Deklarationen bietet, wobei ein ausgelassener ::Type ::Any entspricht.

julia> typeof((a=1,b="hello")) # prints in macro form
@NamedTuple{a::Int64, b::String}

julia> NamedTuple{(:a, :b), Tuple{Int64, String}} # long form of the type
@NamedTuple{a::Int64, b::String}

Die begin ... end-Form des @NamedTuple-Makros ermöglicht es, die Deklarationen über mehrere Zeilen zu verteilen (ähnlich einer Strukturdeklaration), ist jedoch ansonsten gleichwertig:

julia> @NamedTuple begin
           a::Int
           b::String
       end
@NamedTuple{a::Int64, b::String}

Ein NamedTuple-Typ kann als Konstruktor verwendet werden, der ein einzelnes Tupel-Argument akzeptiert. Der konstruierte NamedTuple-Typ kann entweder ein konkreter Typ sein, bei dem beide Parameter angegeben sind, oder ein Typ, der nur Feldnamen angibt:

julia> @NamedTuple{a::Float32,b::String}((1, ""))
(a = 1.0f0, b = "")

julia> NamedTuple{(:a, :b)}((1, ""))
(a = 1, b = "")

Wenn die Feldtypen angegeben sind, werden die Argumente konvertiert. Andernfalls werden die Typen der Argumente direkt verwendet.

Parametric Primitive Types

Primitive-Typen können auch parametrisch deklariert werden. Zum Beispiel werden Zeiger als primitive Typen dargestellt, die in Julia wie folgt deklariert werden:

# 32-bit system:
primitive type Ptr{T} 32 end

# 64-bit system:
primitive type Ptr{T} 64 end

Die leicht seltsame Eigenschaft dieser Deklarationen im Vergleich zu typischen parametrischen zusammengesetzten Typen besteht darin, dass der Typparameter T in der Definition des Typs selbst nicht verwendet wird – er ist nur ein abstrakter Tag, der im Wesentlichen eine gesamte Familie von Typen mit identischer Struktur definiert, die sich nur durch ihren Typparameter unterscheiden. Daher sind Ptr{Float64} und Ptr{Int64} unterschiedliche Typen, obwohl sie identische Darstellungen haben. Und natürlich sind alle spezifischen Zeigertypen Untertypen des übergeordneten Ptr Typs:

julia> Ptr{Float64} <: Ptr
true

julia> Ptr{Int64} <: Ptr
true

UnionAll Types

Wir haben gesagt, dass ein parametrischer Typ wie Ptr als Supertyp aller seiner Instanzen (Ptr{Int64} usw.) fungiert. Wie funktioniert das? Ptr selbst kann kein normaler Datentyp sein, da der Typ der referenzierten Daten ohne Kenntnis nicht für Speicheroperationen verwendet werden kann. Die Antwort ist, dass Ptr (oder andere parametrische Typen wie Array) eine andere Art von Typ ist, die als UnionAll-Typ bezeichnet wird. Ein solcher Typ drückt die iterierte Vereinigung von Typen für alle Werte eines bestimmten Parameters aus.

UnionAll-Typen werden normalerweise mit dem Schlüsselwort where geschrieben. Zum Beispiel könnte Ptr genauer als Ptr{T} where T geschrieben werden, was bedeutet, dass alle Werte, deren Typ Ptr{T} für einen bestimmten Wert von T ist. In diesem Kontext wird der Parameter T auch oft als "Typvariable" bezeichnet, da er wie eine Variable ist, die über Typen variiert. Jede where-Klausel führt eine einzelne Typvariable ein, sodass diese Ausdrücke für Typen mit mehreren Parametern geschachtelt sind, zum Beispiel Array{T,N} where N where T.

Die Typanwendungssyntax A{B,C} erfordert, dass A ein UnionAll-Typ ist, und ersetzt zuerst B für die äußerste Typvariable in A. Das Ergebnis wird als ein weiterer UnionAll-Typ erwartet, in den dann C substituiert wird. Daher ist A{B,C} äquivalent zu A{B}{C}. Dies erklärt, warum es möglich ist, einen Typ teilweise zu instanziieren, wie zum Beispiel Array{Float64}: Der erste Parameterwert wurde festgelegt, aber der zweite variiert über alle möglichen Werte. Mit der expliziten where-Syntax können beliebige Teilmengen von Parametern festgelegt werden. Zum Beispiel kann der Typ aller eindimensionalen Arrays als Array{T,1} where T geschrieben werden.

Typvariablen können durch Subtypbeziehungen eingeschränkt werden. Array{T} where T<:Integer bezieht sich auf alle Arrays, deren Elementtyp eine Art von Integer ist. Die Syntax Array{<:Integer} ist eine praktische Abkürzung für Array{T} where T<:Integer. Typvariablen können sowohl untere als auch obere Grenzen haben. Array{T} where Int<:T<:Number bezieht sich auf alle Arrays von Numbers, die in der Lage sind, Ints zu enthalten (da T mindestens so groß wie Int sein muss). Die Syntax where T>:Int funktioniert ebenfalls, um nur die untere Grenze einer Typvariablen anzugeben, und Array{>:Int} ist äquivalent zu Array{T} where T>:Int.

Da where-Ausdrücke geschachtelt sind, können Typvariablenbeschränkungen auf äußere Typvariablen verweisen. Zum Beispiel bezieht sich Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real auf 2-Tupel, deren erstes Element ein Real ist und dessen zweites Element ein Array beliebiger Art von Array ist, dessen Elementtyp den Typ des ersten Tupel-Elements enthält.

Das where-Schlüsselwort kann selbst in einer komplexeren Deklaration geschachtelt werden. Betrachten Sie beispielsweise die beiden Typen, die durch die folgenden Deklarationen erstellt wurden:

julia> const T1 = Array{Array{T, 1} where T, 1}
Vector{Vector} (alias for Array{Array{T, 1} where T, 1})

julia> const T2 = Array{Array{T, 1}, 1} where T
Array{Vector{T}, 1} where T

Der Typ T1 definiert ein eindimensionales Array von eindimensionalen Arrays; jedes der inneren Arrays besteht aus Objekten desselben Typs, aber dieser Typ kann von einem inneren Array zum nächsten variieren. Andererseits definiert der Typ T2 ein eindimensionales Array von eindimensionalen Arrays, deren innere Arrays alle denselben Typ haben müssen. Beachten Sie, dass T2 ein abstrakter Typ ist, z. B. Array{Array{Int,1},1} <: T2, während T1 ein konkreter Typ ist. Infolgedessen kann T1 mit einem Null-Argument-Konstruktor a=T1() konstruiert werden, T2 jedoch nicht.

Es gibt eine praktische Syntax zur Benennung solcher Typen, ähnlich der Kurzform der Funktionsdefinitionssyntax:

Vector{T} = Array{T, 1}

Dies entspricht const Vector = Array{T,1} where T. Das Schreiben von Vector{Float64} ist äquivalent zum Schreiben von Array{Float64,1}, und der Übertyp Vector hat als Instanzen alle Array-Objekte, bei denen der zweite Parameter – die Anzahl der Array-Dimensionen – 1 ist, unabhängig davon, welcher Elementtyp vorliegt. In Sprachen, in denen parametrische Typen immer vollständig angegeben werden müssen, ist dies nicht besonders hilfreich, aber in Julia ermöglicht es, einfach Vector für den abstrakten Typ zu schreiben, der alle eindimensionalen dichten Arrays eines beliebigen Elementtyps umfasst.

Singleton types

Unveränderliche zusammengesetzte Typen ohne Felder werden Singletons genannt. Formal, wenn

  1. T ist ein unveränderlicher zusammengesetzter Typ (d.h. definiert mit struct),
  2. a ist T && b ist T impliziert a === b,

dann ist T ein Singleton-Typ.[2] Base.issingletontype kann verwendet werden, um zu überprüfen, ob ein Typ ein Singleton-Typ ist. Abstract types können von der Konstruktion her keine Singleton-Typen sein.

Aus der Definition folgt, dass es nur eine Instanz solcher Typen geben kann:

julia> struct NoFields
       end

julia> NoFields() === NoFields()
true

julia> Base.issingletontype(NoFields)
true

Die ===-Funktion bestätigt, dass die konstruierten Instanzen von NoFields tatsächlich ein und dieselbe sind.

Parametrische Typen können Singleton-Typen sein, wenn die oben genannte Bedingung erfüllt ist. Zum Beispiel,

julia> struct NoFieldsParam{T}
       end

julia> Base.issingletontype(NoFieldsParam) # Can't be a singleton type ...
false

julia> NoFieldsParam{Int}() isa NoFieldsParam # ... because it has ...
true

julia> NoFieldsParam{Bool}() isa NoFieldsParam # ... multiple instances.
true

julia> Base.issingletontype(NoFieldsParam{Int}) # Parametrized, it is a singleton.
true

julia> NoFieldsParam{Int}() === NoFieldsParam{Int}()
true

Types of functions

Jede Funktion hat ihren eigenen Typ, der ein Subtyp von Function ist.

julia> foo41(x) = x + 1
foo41 (generic function with 1 method)

julia> typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)

Beachten Sie, wie typeof(foo41) sich selbst ausgibt. Dies ist lediglich eine Konvention für die Ausgabe, da es sich um ein erstklassiges Objekt handelt, das wie jeder andere Wert verwendet werden kann:

julia> T = typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)

julia> T <: Function
true

Arten von Funktionen, die auf der obersten Ebene definiert sind, sind Singletons. Wenn nötig, können Sie sie mit === vergleichen.

Closures haben auch ihren eigenen Typ, der normalerweise mit Namen gedruckt wird, die mit #<number> enden. Namen und Typen für Funktionen, die an verschiedenen Orten definiert sind, sind unterschiedlich, aber es ist nicht garantiert, dass sie in verschiedenen Sitzungen auf die gleiche Weise gedruckt werden.

julia> typeof(x -> x + 1)
var"#9#10"

Arten von Closures sind nicht unbedingt Singletons.

julia> addy(y) = x -> x + y
addy (generic function with 1 method)

julia> typeof(addy(1)) === typeof(addy(2))
true

julia> addy(1) === addy(2)
false

julia> Base.issingletontype(typeof(addy(1)))
false

Type{T} type selectors

Für jeden Typ T ist Type{T} ein abstrakter parametrischer Typ, dessen einzige Instanz das Objekt T ist. Bis wir Parametric Methods und conversions besprechen, ist es schwierig, den Nutzen dieses Konstrukts zu erklären, aber kurz gesagt, es ermöglicht, das Verhalten von Funktionen auf spezifische Typen als Werte zu spezialisieren. Dies ist nützlich, um Methoden (insbesondere parametrische) zu schreiben, deren Verhalten von einem Typ abhängt, der als explizites Argument angegeben wird, anstatt durch den Typ eines ihrer Argumente impliziert zu werden.

Da die Definition etwas schwer zu verstehen ist, schauen wir uns einige Beispiele an:

julia> isa(Float64, Type{Float64})
true

julia> isa(Real, Type{Float64})
false

julia> isa(Real, Type{Real})
true

julia> isa(Float64, Type{Real})
false

Mit anderen Worten, isa(A, Type{B}) ist wahr, wenn und nur wenn A und B dasselbe Objekt sind und dieses Objekt ein Typ ist.

Insbesondere, da parametrische Typen invariant sind, haben wir

julia> struct TypeParamExample{T}
           x::T
       end

julia> TypeParamExample isa Type{TypeParamExample}
true

julia> TypeParamExample{Int} isa Type{TypeParamExample}
false

julia> TypeParamExample{Int} isa Type{TypeParamExample{Int}}
true

Ohne den Parameter ist Type einfach ein abstrakter Typ, der alle Typobjekte als seine Instanzen hat:

julia> isa(Type{Float64}, Type)
true

julia> isa(Float64, Type)
true

julia> isa(Real, Type)
true

Jedes Objekt, das kein Typ ist, ist keine Instanz von Type:

julia> isa(1, Type)
false

julia> isa("foo", Type)
false

Während Type Teil von Julias Typ-Hierarchie ist, wie jeder andere abstrakte parametrische Typ, wird es außerhalb von Methodensignaturen nicht häufig verwendet, außer in einigen speziellen Fällen. Ein weiterer wichtiger Anwendungsfall für Type ist das Schärfen von Feldtypen, die ansonsten weniger präzise erfasst werden würden, z. B. als DataType im folgenden Beispiel, wo der Standardkonstruktor zu Leistungsproblemen in Code führen könnte, der auf den präzisen umschlossenen Typ angewiesen ist (ähnlich wie abstract type parameters).

julia> struct WrapType{T}
       value::T
       end

julia> WrapType(Float64) # default constructor, note DataType
WrapType{DataType}(Float64)

julia> WrapType(::Type{T}) where T = WrapType{Type{T}}(T)
WrapType

julia> WrapType(Float64) # sharpened constructor, note more precise Type{Float64}
WrapType{Type{Float64}}(Float64)

Type Aliases

Manchmal ist es praktisch, einen neuen Namen für einen bereits ausdrückbaren Typ einzuführen. Dies kann mit einer einfachen Zuweisungsanweisung erfolgen. Zum Beispiel wird UInt entweder auf UInt32 oder UInt64 aliasiert, je nach Größe der Zeiger im System:

# 32-bit system:
julia> UInt
UInt32

# 64-bit system:
julia> UInt
UInt64

Dies wird durch den folgenden Code in base/boot.jl erreicht:

if Int === Int64
    const UInt = UInt64
else
    const UInt = UInt32
end

Natürlich hängt das davon ab, was Int aliasiert ist – aber das ist vordefiniert, um den richtigen Typ zu sein – entweder Int32 oder Int64.

(Hinweis: Im Gegensatz zu Int existiert Float nicht als Typalias für eine spezifische Größe AbstractFloat. Im Gegensatz zu Ganzzahlregistern, bei denen die Größe von Int der Größe eines nativen Zeigers auf dieser Maschine entspricht, werden die Größen der Gleitkommaregister durch den IEEE-754-Standard festgelegt.)

Typaliasen können parametrisiert werden:

julia> const Family{T} = Set{T}
Set

julia> Family{Char} === Set{Char}
true

Operations on Types

Da Typen in Julia selbst Objekte sind, können gewöhnliche Funktionen auf ihnen operieren. Einige Funktionen, die besonders nützlich sind, um mit Typen zu arbeiten oder sie zu erkunden, wurden bereits eingeführt, wie der <:-Operator, der angibt, ob sein linkes Operanden ein Subtyp seines rechten Operanden ist.

Die isa-Funktion testet, ob ein Objekt von einem bestimmten Typ ist, und gibt true oder false zurück:

julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false

Die typeof-Funktion, die bereits im gesamten Handbuch in Beispielen verwendet wurde, gibt den Typ ihres Arguments zurück. Da, wie oben erwähnt, Typen Objekte sind, haben sie auch Typen, und wir können fragen, was ihre Typen sind:

julia> typeof(Rational{Int})
DataType

julia> typeof(Union{Real,String})
Union

Was passiert, wenn wir den Prozess wiederholen? Was ist der Typ eines Typs eines Typs? Wie es der Fall ist, sind Typen alle zusammengesetzte Werte und haben daher alle den Typ DataType:

julia> typeof(DataType)
DataType

julia> typeof(Union)
DataType

DataType ist sein eigener Typ.

Eine weitere Operation, die auf einige Typen angewendet wird, ist supertype, die den Supertyp eines Typs offenbart. Nur deklarierte Typen (DataType) haben eindeutige Supertypen:

julia> supertype(Float64)
AbstractFloat

julia> supertype(Number)
Any

julia> supertype(AbstractString)
Any

julia> supertype(Any)
Any

Wenn Sie supertype auf andere Typobjekte (oder Nicht-Typobjekte) anwenden, wird ein MethodError ausgelöst:

julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
The function `supertype` exists, but no method is defined for this combination of argument types.

Closest candidates are:
[...]

Custom pretty-printing

Oft möchte man anpassen, wie Instanzen eines Typs angezeigt werden. Dies wird erreicht, indem die show-Funktion überladen wird. Zum Beispiel, nehmen wir an, wir definieren einen Typ, um komplexe Zahlen in polarer Form darzustellen:

julia> struct Polar{T<:Real} <: Number
           r::T
           Θ::T
       end

julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar

Hier haben wir eine benutzerdefinierte Konstruktorfunktion hinzugefügt, damit sie Argumente verschiedener Real-Typen annehmen und sie in einen gemeinsamen Typ umwandeln kann (siehe Constructors und Conversion and Promotion). (Natürlich müssten wir auch viele andere Methoden definieren, um es wie ein Number agieren zu lassen, z.B. +, *, one, zero, Promotionsregeln usw.) Standardmäßig zeigen Instanzen dieses Typs recht einfach Informationen über den Typnamen und die Feldwerte an, z.B. Polar{Float64}(3.0,4.0).

Wenn wir möchten, dass es stattdessen als 3.0 * exp(4.0im) angezeigt wird, würden wir die folgende Methode definieren, um das Objekt an ein bestimmtes Ausgabegerät io (das eine Datei, ein Terminal, einen Puffer usw. darstellt; siehe Networking and Streams) auszugeben:

julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")

Eine feinere Steuerung über die Anzeige von Polar-Objekten ist möglich. Insbesondere möchte man manchmal sowohl ein ausführliches mehrzeiliges Druckformat, das zur Anzeige eines einzelnen Objekts in der REPL und anderen interaktiven Umgebungen verwendet wird, als auch ein kompakteres einzeiliges Format, das für print oder zur Anzeige des Objekts als Teil eines anderen Objekts (z. B. in einem Array) verwendet wird. Obwohl standardmäßig die Funktion show(io, z) in beiden Fällen aufgerufen wird, können Sie ein anderes mehrzeiliges Format zur Anzeige eines Objekts definieren, indem Sie eine dreiarugumentige Form von show überladen, die den MIME-Typ text/plain als zweiten Parameter verwendet (siehe Multimedia I/O), zum Beispiel:

julia> Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) where{T} =
           print(io, "Polar{$T} complex number:\n   ", z)

(Beachten Sie, dass print(..., z) hier die 2-Argumente-Methode show(io, z) aufruft.) Dies führt zu:

julia> Polar(3, 4.0)
Polar{Float64} complex number:
   3.0 * exp(4.0im)

julia> [Polar(3, 4.0), Polar(4.0,5.3)]
2-element Vector{Polar{Float64}}:
 3.0 * exp(4.0im)
 4.0 * exp(5.3im)

wo die einzeilige show(io, z)-Form weiterhin für ein Array von Polar-Werten verwendet wird. Technisch gesehen ruft die REPL display(z) auf, um das Ergebnis der Ausführung einer Zeile anzuzeigen, was standardmäßig show(stdout, MIME("text/plain"), z) entspricht, was wiederum standardmäßig show(stdout, z) entspricht, aber Sie sollten keine neuen display-Methoden definieren, es sei denn, Sie definieren einen neuen Multimedia-Anzeigebehandler (siehe Multimedia I/O).

Darüber hinaus können Sie auch show-Methoden für andere MIME-Typen definieren, um eine reichhaltigere Anzeige (HTML, Bilder usw.) von Objekten in Umgebungen zu ermöglichen, die dies unterstützen (z. B. IJulia). Zum Beispiel können wir die formatierte HTML-Anzeige von Polar-Objekten mit Hochzahlen und Kursivschrift definieren, über:

julia> Base.show(io::IO, ::MIME"text/html", z::Polar{T}) where {T} =
           println(io, "<code>Polar{$T}</code> complex number: ",
                   z.r, " <i>e</i><sup>", z.Θ, " <i>i</i></sup>")

Ein Polar-Objekt wird dann automatisch mit HTML in einer Umgebung angezeigt, die die HTML-Darstellung unterstützt, aber Sie können show manuell aufrufen, um HTML-Ausgaben zu erhalten, wenn Sie möchten:

julia> show(stdout, "text/html", Polar(3.0,4.0))
<code>Polar{Float64}</code> complex number: 3.0 <i>e</i><sup>4.0 <i>i</i></sup>

An HTML renderer would display this as: Polar{Float64} complex number: 3.0 e4.0 i

Als Faustregel sollte die einzeilige show-Methode einen gültigen Julia-Ausdruck drucken, um das angezeigte Objekt zu erstellen. Wenn diese show-Methode infix-Operatoren enthält, wie zum Beispiel den Multiplikationsoperator (*) in unserer einzeiligen show-Methode für Polar oben, kann es sein, dass sie nicht korrekt geparst wird, wenn sie als Teil eines anderen Objekts gedruckt wird. Um dies zu sehen, betrachten Sie das Ausdrucksobjekt (siehe Program representation), das das Quadrat einer bestimmten Instanz unseres Polar-Typs nimmt:

julia> a = Polar(3, 4.0)
Polar{Float64} complex number:
   3.0 * exp(4.0im)

julia> print(:($a^2))
3.0 * exp(4.0im) ^ 2

Weil der Operator ^ eine höhere Priorität als * hat (siehe Operator Precedence and Associativity), stellt diese Ausgabe den Ausdruck a ^ 2 nicht treu dar, der gleich (3.0 * exp(4.0im)) ^ 2 sein sollte. Um dieses Problem zu lösen, müssen wir eine benutzerdefinierte Methode für Base.show_unquoted(io::IO, z::Polar, indent::Int, precedence::Int) erstellen, die intern vom Ausdrucksobjekt beim Drucken aufgerufen wird:

julia> function Base.show_unquoted(io::IO, z::Polar, ::Int, precedence::Int)
           if Base.operator_precedence(:*) <= precedence
               print(io, "(")
               show(io, z)
               print(io, ")")
           else
               show(io, z)
           end
       end

julia> :($a^2)
:((3.0 * exp(4.0im)) ^ 2)

Die oben definierte Methode fügt Klammern um den Aufruf von show hinzu, wenn die Priorität des aufrufenden Operators höher oder gleich der Priorität der Multiplikation ist. Diese Überprüfung ermöglicht es, Ausdrücke, die ohne die Klammern korrekt geparst werden (wie :($a + 2) und :($a == 2)), beim Drucken wegzulassen:

julia> :($a + 2)
:(3.0 * exp(4.0im) + 2)

julia> :($a == 2)
:(3.0 * exp(4.0im) == 2)

In einigen Fällen ist es nützlich, das Verhalten von show-Methoden je nach Kontext anzupassen. Dies kann über den IOContext-Typ erreicht werden, der es ermöglicht, kontextuelle Eigenschaften zusammen mit einem umschlossenen IO-Stream zu übergeben. Zum Beispiel können wir eine kürzere Darstellung in unserer show-Methode erstellen, wenn die :compact-Eigenschaft auf true gesetzt ist, und auf die lange Darstellung zurückgreifen, wenn die Eigenschaft false oder nicht vorhanden ist:

julia> function Base.show(io::IO, z::Polar)
           if get(io, :compact, false)::Bool
               print(io, z.r, "ℯ", z.Θ, "im")
           else
               print(io, z.r, " * exp(", z.Θ, "im)")
           end
       end

Diese neue kompakte Darstellung wird verwendet, wenn der übergebene IO-Stream ein IOContext-Objekt mit der :compact-Eigenschaft gesetzt ist. Insbesondere ist dies der Fall, wenn Arrays mit mehreren Spalten (wo der horizontale Platz begrenzt ist) gedruckt werden:

julia> show(IOContext(stdout, :compact=>true), Polar(3, 4.0))
3.0ℯ4.0im

julia> [Polar(3, 4.0) Polar(4.0,5.3)]
1×2 Matrix{Polar{Float64}}:
 3.0ℯ4.0im  4.0ℯ5.3im

Siehe die IOContext Dokumentation für eine Liste von gängigen Eigenschaften, die verwendet werden können, um das Drucken anzupassen.

"Value types"

In Julia kannst du nicht auf einen Wert wie true oder false dispatchen. Du kannst jedoch auf parametrische Typen dispatchen, und Julia erlaubt es dir, "einfache Bits"-Werte (Typen, Symbole, Ganzzahlen, Fließkommazahlen, Tupel usw.) als Typparameter einzuschließen. Ein häufiges Beispiel ist der Dimensionsparameter in Array{T,N}, wobei T ein Typ ist (z. B. Float64), aber N ist einfach ein Int.

Sie können Ihre eigenen benutzerdefinierten Typen erstellen, die Werte als Parameter annehmen, und diese verwenden, um die Zustellung benutzerdefinierter Typen zu steuern. Um diese Idee zu veranschaulichen, lassen Sie uns den parametrischen Typ Val{x} einführen und seinen Konstruktor Val(x) = Val{x}(), der als übliche Methode dient, um diese Technik in Fällen zu nutzen, in denen Sie keine ausgeklügeltere Hierarchie benötigen.

Val wird definiert als:

julia> struct Val{x}
       end

julia> Val(x) = Val{x}()
Val

Es gibt nichts Weiteres zur Implementierung von Val als dies. Einige Funktionen in Julias Standardbibliothek akzeptieren Val-Instanzen als Argumente, und Sie können es auch verwenden, um Ihre eigenen Funktionen zu schreiben. Zum Beispiel:

julia> firstlast(::Val{true}) = "First"
firstlast (generic function with 1 method)

julia> firstlast(::Val{false}) = "Last"
firstlast (generic function with 2 methods)

julia> firstlast(Val(true))
"First"

julia> firstlast(Val(false))
"Last"

Für Konsistenz in Julia sollte der Aufruf immer eine Val Instanz übergeben, anstatt einen Typ zu verwenden, d.h. verwende foo(Val(:bar)) anstelle von foo(Val{:bar}).

Es ist erwähnenswert, dass es extrem einfach ist, parametrische "Wert"-Typen, einschließlich Val, falsch zu verwenden; in ungünstigen Fällen kann es leicht passieren, dass die Leistung Ihres Codes viel schlechter wird. Insbesondere sollten Sie niemals tatsächlichen Code wie oben dargestellt schreiben. Für weitere Informationen über die richtigen (und falschen) Verwendungen von Val lesen Sie bitte the more extensive discussion in the performance tips.

  • 1"Small" is defined by the max_union_splitting configuration, which currently defaults to 4.
  • 2A few popular languages have singleton types, including Haskell, Scala and Ruby.