Ty

Typen in Julia

1 Übung

Über Typen

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.

  1. Julia hat types, die zentral für sein Design sind.
  2. Julia kann den Typ meistens „erraten“, und zwar mithilfe von 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.

Typ-Promotion

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)

Typzuweisung

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:

  • In kleinen Skripten sehr praktisch.
  • Schlecht für Leistung und Zuverlässigkeit in größeren Programmen.

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.

Typ-Assertion

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.

Die Typhierarchie

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.
  • Es gibt eine Hierarchie von Typen, die über 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.

Typen testen

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.

Abstrakte und konkrete Typen

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:

  1. Knoten mit Subtypen heißen abstract.
  2. Blattknoten ohne Subtypen heißen concrete.
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
Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab

Lerne Typen