Bisher hat der Lehrplan nicht viel über Typen gesagt, aber offensichtlich gibt es sie in Julia:
julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')
julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}
Wir haben die Typen nie angegeben, aber Julia hat sie trotzdem zugewiesen.
types, die zentral für sein Design sind.Type Inference.Der JIT-Compiler schaut sich den (gesamten) Code an, sieht, wie eine Variable verwendet wird, und leitet einen passenden Standardtyp ab, der zu dieser Verwendung passt.
In früheren Konzepten gab es viele Beispiele, bei denen numerische Typen vereinheitlicht wurden, angefangen bei der einfachsten Arithmetik.
julia> 2 + 1.3
3.3
Wir haben eine Ganzzahl (Int64) und eine Gleitkommazahl (Float64) addiert und ein Float64-Ergebnis erhalten. Ähnlich hier:
julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)
julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}
julia> [nums...]
3-element Vector{Float64}:
3.0
4.1
0.25
Im obigen Fall hat ein Tupel den Typ jedes Elements beibehalten, aber die Umwandlung in einen Vektor hat alle in ein einheitliches Float64 geändert.
Der Julia-Compiler versteht, welche Typkonvertierungen möglich sind: von Ganzzahl zu Gleitkommazahl ist kein Problem, von Gleitkommazahl zu Ganzzahl verliert man Präzision, also wird ein InexactError ausgelöst.
Die Typ-Promotion wandelt alle Werte im Ausdruck in einen gemeinsamen Typ um, der vielseitig genug ist, um mit allen Eingaben kompatibel zu sein.
Dieselbe Konvertierung kann explizit mit der Funktion promote() durchgeführt werden:
julia> promote(nums...)
(3.0, 4.1, 0.25)
Für das Lösen einfacher Übungen ist es in Ordnung, sich auf Typinferenz und Typ-Promotion zu verlassen, aber für größere Programme brauchst du wahrscheinlich eine genauere Kontrolle.
Auf den meisten modernen Prozessoren ist der Standardtyp für eine Ganzzahl Int64.
Im Konzept Numbers haben wir gesehen, dass ein Wert in einen bestimmten Nicht-Standard-Typ umgewandelt werden kann.
julia> x = Int16(42)
42
julia> typeof(x)
Int16
Die Variable x kann jedoch trotzdem einem anderen Typ neu zugewiesen werden:
julia> x = "changed"
"changed"
julia> typeof(x)
String
Das nennt man type instability, und zwar:
Stattdessen können wir den Typ von x mit dem Operator :: festlegen:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.
Jetzt ist y vom Typ Int16 und wird es immer bleiben.
Somit weiß der Compiler, wie viele Bytes er dafür reservieren muss, und kann den übrigen Code darauf optimieren, sich auf einen stabilen Typ zu verlassen.
Damit ähnelt die Variable mehr oder weniger denen in einer statisch typisierten Sprache wie C.
Die oben beschriebene Typzuweisung wird auf der linken Seite einer Variablenzuweisung verwendet, um den Typ dieser Variablen einzuschränken.
Verwendet man den Operator :: mit einem Wert oder mit etwas, das zu einem Wert ausgewertet wird, ist das im Allgemeinen eine Assertion, dass der Wert von diesem Typ sein muss, andernfalls sollte ein Fehler ausgelöst werden.
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
Häufig wird das mit dem Rückgabewert einer Funktion verwendet, als einfache letzte Prüfung, dass die Funktion sich wie erwartet verhalten hat.
Beachte, dass es ein Makro @assert für andere Formen der Assertion gibt.
Int64, Int16, String, Char: Woher „kommen“ diese Typen eigentlich?
In vielen objektorientierten Sprachen (OO) ist jeder Typ eine Klasse, die Vererbung ordnet sie in einer Klassenhierarchie an, und Klassenmethoden definieren das Verhalten.
Java und Ruby sind offensichtliche Beispiele für dieses Muster, aber selbst Python ist intern ähnlich.
Julia hat keine Klassen.
Der dokumentierte Grund ist, dass OO-Features den JIT-Compiler stören und die Laufzeitleistung beeinträchtigen.
Wer weiß, vielleicht haben sie auch dieses Zitat gelesen:
„Objektorientierte Programmierung ist eine außergewöhnlich schlechte Idee, die nur aus Kalifornien stammen konnte.“
Sie wird Edsger Dijkstra zugeschrieben, einem brillanten Informatiker, der ab den 1950er Jahren mehrere Jahrzehnte lang tätig war (auch wenn er nicht gerade für seinen sonnigen Optimismus oder seine subtile Diplomatie bekannt war).
Und doch, schau dir diesen Code an:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> supertype(Int16)
Signed
julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)
Ein paar Details dazu:
Int16 ist ein Typ, und wir können Variablen dieses Typs erstellen.Int16 ist ein Subtyp von Signed, und die Funktion supertype() zeigt uns das.Integer, Real und Number bis zu Any an der Spitze hinaufgeht, und subtypes() listet diesen Zweig der Hierarchie für uns auf.Alle Zweige enden in Any, das als einziges sein eigener Supertyp ist.
julia> supertypes(String)
(String, AbstractString, Any)
julia> supertype(Any)
Any
Julia hat also keine class-Hierarchie, aber es hat eine type-Hierarchie.
Der Versuch, die gesamte Hierarchie darzustellen, ergibt einen riesigen Baum, den man unmöglich wirklich ansehen kann.
Teile davon anzuschauen wird online diskutiert.
Irgendwann werden wir versuchen zu entschlüsseln, wie das funktioniert, aber zuerst gibt es noch viel mehr zu entdecken.
Wir können typeof() verwenden, um wie gewohnt auf Gleichheit zu testen.
julia> typeof(11)
Int64
julia> typeof(11) == Int64
true
julia> typeof(11) == Number
false
Die Typgleichheit muss exakt sein, denn diese Form des Vergleichs kennt die Typhierarchie nicht.
Flexibler ist isa: Es sagt uns, ob ein Wert denselben Typ wie ein Vergleichswert hat oder einen Subtyp davon.
Man kann es sowohl in Infix- als auch in Funktionsform verwenden.
julia> 12 isa Int64
true
julia> 12 isa Number
true
julia> isa(12, Number)
true
julia> 12 isa String
false
Beachte, dass isa links einen value erwartet, keinen type.
Zwei Typen auf diese Weise zu vergleichen, liefert unerwartete Ergebnisse.
Der richtige Operator ist <:, den wir in zukünftigen Konzepten noch viel häufiger sehen werden.
julia> Int64 isa Number ## Don't do this!
false
julia> Int64 <: Number
true
Das Testen von Typen kann man zur Steuerung des Ablaufs innerhalb einer Funktion verwenden, aber in idiomatischem Julia ist das relativ ungewöhnlich.
Wir werden im Multiple-Dispatch-Konzept sehen, dass es oft effizienter ist, Typen zu den Funktionsargumenten hinzuzufügen und Julias Dispatch-Mechanismus diese Logik übernehmen zu lassen. Es gibt jedoch noch mehrere weitere typbezogene Konzepte, die wir besprechen müssen, bevor wir bei Multiple Dispatch ankommen.
Wir haben gesehen, dass die Typhierarchie eine Baumstruktur bildet (im Sinne der Informatik, mit der Wurzel oben).
Jedes Element im Baum ist ein node, und diese lassen sich in Kategorien einteilen:
julia> subtypes(Integer) # an abstract type
3-element Vector{Any}:
Bool
Signed
Unsigned
julia> subtypes(Int64) # a concrete type
Type[]
Das ist ein wichtiger Unterschied, denn nur konkrete Typen können als Variablen instantiated werden.
julia> a::Int16 = 42
42
julia> typeof(a)
Int16
julia> b::Integer = 42
42
julia> typeof(b)
Int64
Beachte, dass der Versuch, einen abstrakten Typ zu verwenden, (in diesem Fall) keine Fehlermeldung erzeugt, aber der Compiler erstellt einen passenden konkreten Typ: Int64 statt Integer.
Zumindest verhindert es, dass der Variablen etwas zugewiesen wird, das keine Ganzzahl ist:
julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.
Eine Typzuweisung mit einem abstrakten Typ ist also eine Einschränkung des Variablentyps auf einen beliebigen Subtyp von (im obigen Fall) Integer.
Das ist in der Welt der Programmiersprachen ungewöhnlich: schwächer als eine Typzuweisung in C, stärker als ein Typhinweis in neueren Python-Versionen.
Die Funktionen isabstracttype() und isconcretetype() ermöglichen Tests.
Beachte, dass sie nicht einfach Negationen voneinander sind: In einem späteren Konzept werden wir sehen, dass manche Typen weder abstrakt noch konkret sein können.
julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)
julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)
# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)
Obwohl Vector kein konkreter Typ ist, sind die Elemente konkret.
Die Funktion eltype() (Elementtyp) extrahiert diesen Typ:
julia> eltype([1, 2.3])
Float64