Μέχρι τώρα, το πρόγραμμα σπουδών δεν έχει πει πολλά για τους τύπους, αλλά σαφώς υπάρχουν στη Julia:
julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')
julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}
Δεν προσδιορίσαμε ποτέ τους τύπους, αλλά η Julia τους απέδωσε ούτως ή άλλως.
types, που είναι κεντρικοί στον σχεδιασμό της.Type Inference.Ο μεταγλωττιστής JIT θα εξετάσει (όλο) τον κώδικα, θα δει πώς χρησιμοποιείται μια μεταβλητή και θα συμπεράνει έναν κατάλληλο προεπιλεγμένο τύπο συμβατό με αυτή τη χρήση.
Υπήρξαν πολλά παραδείγματα σε προηγούμενες έννοιες όπου αριθμητικοί τύποι εξαναγκάζονται σε ομοιομορφία, ξεκινώντας από την απλούστερη αριθμητική.
julia> 2 + 1.3
3.3
Προσθέσαμε έναν ακέραιο (Int64) και έναν αριθμό κινητής υποδιαστολής (Float64) και πήραμε αποτέλεσμα Float64. Παρόμοια:
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
Στην παραπάνω περίπτωση, μια πλειάδα διατήρησε τον τύπο κάθε στοιχείου, αλλά η μετατροπή σε διάνυσμα τους άλλαξε όλους σε ομοιόμορφο Float64.
Ο μεταγλωττιστής της Julia καταλαβαίνει ποιες μετατροπές τύπων είναι δυνατές: από ακέραιο σε κινητής υποδιαστολής δεν υπάρχει πρόβλημα, από κινητής υποδιαστολής σε ακέραιο χάνεται ακρίβεια, οπότε η Julia πετάει ένα InexactError.
Η προαγωγή τύπων μετατρέπει όλες τις τιμές της παράστασης σε έναν κοινό τύπο, αρκετά ευέλικτο ώστε να είναι συμβατός με όλες τις εισόδους.
Η ίδια μετατροπή μπορεί να γίνει ρητά με τη συνάρτηση promote():
julia> promote(nums...)
(3.0, 4.1, 0.25)
Το να βασίζεσαι στη συμπερασματολογία τύπων και στην προαγωγή τύπων είναι εντάξει για την επίλυση απλών ασκήσεων εκμάθησης, αλλά για μεγαλύτερα προγράμματα πιθανότατα θα χρειαστείς πιο ακριβή έλεγχο.
Στους περισσότερους σύγχρονους επεξεργαστές, ένας ακέραιος έχει από προεπιλογή τύπο Int64.
Είδαμε στην έννοια Numbers ότι μια τιμή μπορεί να μετατραπεί σε έναν συγκεκριμένο μη προεπιλεγμένο τύπο.
julia> x = Int16(42)
42
julia> typeof(x)
Int16
Ωστόσο, η μεταβλητή x μπορεί ακόμα να εκχωρηθεί ξανά σε διαφορετικό τύπο:
julia> x = "changed"
"changed"
julia> typeof(x)
String
Αυτή είναι η type instability, η οποία είναι:
Αντί γι' αυτό, μπορούμε να ορίσουμε τον τύπο της x χρησιμοποιώντας τον τελεστή :::
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.
Τώρα η y είναι, και πάντα θα είναι, τύπου Int16.
Έτσι, ο μεταγλωττιστής ξέρει πόσα bytes να δεσμεύσει γι' αυτήν και μπορεί να βελτιστοποιήσει τον υπόλοιπο κώδικα ώστε να βασίζεται σε έναν σταθερό τύπο.
Ως προς αυτό, η μεταβλητή μοιάζει λίγο πολύ με εκείνες σε μια γλώσσα με στατικούς τύπους, όπως η C.
Η εκχώρηση τύπου, όπως περιγράφηκε παραπάνω, χρησιμοποιείται στην αριστερή πλευρά μιας εκχώρησης μεταβλητής για να περιορίσει τον τύπο αυτής της μεταβλητής.
Η χρήση του τελεστή :: με μια τιμή, ή με κάτι που αξιολογείται σε μια τιμή, είναι γενικά μια διαβεβαίωση ότι η τιμή πρέπει να είναι αυτού του τύπου, αλλιώς θα πρέπει να πεταχτεί ένα σφάλμα.
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
Συνήθως, αυτό μπορεί να χρησιμοποιηθεί με την τιμή επιστροφής μιας συνάρτησης, ως ένας απλός τελευταίος έλεγχος ότι η συνάρτηση συμπεριφέρθηκε όπως αναμενόταν.
Σημείωσε ότι υπάρχει ένα macro @assert για άλλες μορφές διαβεβαίωσης.
Int64, Int16, String, Char: από πού "προέρχονται" αυτοί οι τύποι;
Σε πολλές αντικειμενοστρεφείς (OO) γλώσσες, κάθε τύπος είναι μια κλάση, η δημιουργία υποκλάσεων τις οργανώνει σε μια ιεραρχία κλάσεων και οι μέθοδοι των κλάσεων ορίζουν τις συμπεριφορές.
Η Java και η Ruby είναι προφανή παραδείγματα αυτού του μοτίβου, αλλά ακόμα και η Python είναι εσωτερικά παρόμοια.
Η Julia δεν έχει κλάσεις.
Ο τεκμηριωμένος λόγος είναι ότι τα χαρακτηριστικά του αντικειμενοστρεφούς προγραμματισμού παρεμβαίνουν στον μεταγλωττιστή JIT και βλάπτουν την απόδοση κατά την εκτέλεση.
Ποιος ξέρει, ίσως διάβασαν και αυτό το απόσπασμα:
"Ο αντικειμενοστρεφής προγραμματισμός είναι μια εξαιρετικά κακή ιδέα που θα μπορούσε να έχει προέλθει μόνο από την Καλιφόρνια."
Αποδίδεται στον Edsger Dijkstra, έναν λαμπρό επιστήμονα υπολογιστών για αρκετές δεκαετίες από τη δεκαετία του 1950 (αν και δεν είναι γενικά γνωστός για τον ηλιόλουστο οπτιμισμό ή τη λεπτή διπλωματία του).
Και όμως, κοίτα αυτόν τον κώδικα:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> supertype(Int16)
Signed
julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)
Συμπληρώνοντας μερικές λεπτομέρειες:
Int16 είναι ένας τύπος και μπορούμε να δημιουργήσουμε μεταβλητές αυτού του τύπου.Int16 είναι υπότυπος του Signed και η συνάρτηση supertype() μας το δείχνει.Integer, Real και Number μέχρι το Any στην κορυφή, και η subtypes() θα μας απαριθμήσει αυτόν τον κλάδο της ιεραρχίας.Όλοι οι κλάδοι καταλήγουν στο Any, το οποίο είναι μοναδικό καθώς είναι ο ίδιος ο υπερτύπος του εαυτού του.
julia> supertypes(String)
(String, AbstractString, Any)
julia> supertype(Any)
Any
Έτσι, η Julia δεν έχει ιεραρχία class, αλλά έχει ιεραρχία type.
Η προσπάθεια να δείξεις ολόκληρη την ιεραρχία δίνει ένα τεράστιο δέντρο, αδύνατο να το δεις πραγματικά.
Το να κοιτάς μέρη της είναι κάτι που συζητείται διαδικτυακά.
Κάποια στιγμή, θα προσπαθήσουμε να ξετυλίξουμε πώς λειτουργεί αυτό, αλλά υπάρχουν πολλά περισσότερα να εξερευνήσουμε πρώτα.
Μπορούμε να χρησιμοποιήσουμε τη typeof() για να ελέγξουμε την ισότητα με τον συνηθισμένο τρόπο.
julia> typeof(11)
Int64
julia> typeof(11) == Int64
true
julia> typeof(11) == Number
false
Η ισότητα τύπων πρέπει να είναι ακριβής, καθώς αυτή η μορφή σύγκρισης δεν κατανοεί την ιεραρχία τύπων.
Με πιο ευέλικτο τρόπο, η isa θα μας πει αν μια τιμή έχει είτε τον ίδιο τύπο με ένα μέτρο σύγκρισης είτε κάποιον υπότυπό του.
Μπορεί να χρησιμοποιηθεί είτε σε ενθηματική μορφή είτε ως συνάρτηση.
julia> 12 isa Int64
true
julia> 12 isa Number
true
julia> isa(12, Number)
true
julia> 12 isa String
false
Σημείωσε ότι η isa περιμένει μια value στα αριστερά, όχι έναν type.
Η προσπάθεια να συγκρίνεις δύο τύπους με αυτόν τον τρόπο θα δώσει απροσδόκητα αποτελέσματα.
Ο σωστός τελεστής είναι το <:, το οποίο θα δούμε πολύ περισσότερο σε μελλοντικές έννοιες.
julia> Int64 isa Number ## Don't do this!
false
julia> Int64 <: Number
true
Ο έλεγχος τύπων μπορεί να χρησιμοποιηθεί για τον έλεγχο ροής μέσα σε μια συνάρτηση, αλλά αυτό είναι σχετικά ασυνήθιστο στην ιδιωματική Julia.
Θα δούμε στην έννοια Multiple Dispatch ότι συχνά είναι πιο αποδοτικό να προσθέτεις τύπους στα ορίσματα των συναρτήσεων και να αφήνεις τον μηχανισμό dispatch της Julia να χειρίζεται τέτοια λογική. Ωστόσο, υπάρχουν πολλές ακόμη έννοιες σχετικές με τους τύπους που πρέπει να συζητήσουμε πριν φτάσουμε στο Multiple Dispatch.
Είδαμε ότι η ιεραρχία τύπων σχηματίζει μια δενδρική δομή (με την έννοια της επιστήμης υπολογιστών, με τη ρίζα στην κορυφή).
Κάθε στοιχείο του δέντρου είναι ένας node, και αυτοί μπορούν να χωριστούν σε κατηγορίες:
abstract.concrete.julia> subtypes(Integer) # an abstract type
3-element Vector{Any}:
Bool
Signed
Unsigned
julia> subtypes(Int64) # a concrete type
Type[]
Αυτή είναι μια σημαντική διάκριση, γιατί μόνο οι συγκεκριμένοι τύποι μπορούν να instantiated ως μεταβλητές.
julia> a::Int16 = 42
42
julia> typeof(a)
Int16
julia> b::Integer = 42
42
julia> typeof(b)
Int64
Σημείωσε ότι η προσπάθεια να χρησιμοποιήσεις έναν αφηρημένο τύπο δεν δίνει μήνυμα σφάλματος (σε αυτή την περίπτωση), αλλά ο μεταγλωττιστής δημιουργεί έναν κατάλληλο συγκεκριμένο τύπο: Int64 αντί για Integer.
Τουλάχιστον εμποδίζει την εκχώρηση της μεταβλητής σε κάτι που δεν είναι ακέραιος:
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.
Η εκχώρηση τύπου με έναν αφηρημένο τύπο είναι επομένως ένας περιορισμός στον τύπο της μεταβλητής, σε οποιονδήποτε υπότυπο του (στην παραπάνω περίπτωση) Integer.
Αυτό είναι κάτι ασυνήθιστο στον κόσμο των γλωσσών προγραμματισμού: πιο αδύναμο από μια εκχώρηση τύπου στη C, πιο ισχυρό από μια υπόδειξη τύπου σε πρόσφατες εκδόσεις της Python.
Οι συναρτήσεις isabstracttype() και isconcretetype() επιτρέπουν τον έλεγχο.
Σημείωσε ότι αυτές δεν είναι απλώς αρνήσεις η μία της άλλης: θα δούμε σε μια επόμενη έννοια ότι κάποιοι τύποι δεν μπορούν να είναι ούτε αφηρημένοι ούτε συγκεκριμένοι.
julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)
julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)
# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)
Αν και το Vector δεν είναι συγκεκριμένος τύπος, τα στοιχεία του είναι συγκεκριμένα.
Η συνάρτηση eltype() (τύπος στοιχείου) εξάγει αυτόν τον τύπο:
julia> eltype([1, 2.3])
Float64