Τύ

Τύποι σε Julia

1 άσκηση

Σχετικά με την έννοια Τύποι

Μέχρι τώρα, το πρόγραμμα σπουδών δεν έχει πει πολλά για τους τύπους, αλλά σαφώς υπάρχουν στη Julia:

julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')

julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}

Δεν προσδιορίσαμε ποτέ τους τύπους, αλλά η Julia τους απέδωσε ούτως ή άλλως.

  1. Η Julia έχει types, που είναι κεντρικοί στον σχεδιασμό της.
  2. Η Julia συνήθως μπορεί να "μαντέψει" τον τύπο, χρησιμοποιώντας 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, και αυτοί μπορούν να χωριστούν σε κατηγορίες:

  1. Οι κόμβοι που έχουν υποτύπους ονομάζονται abstract.
  2. Οι κόμβοι-φύλλα, που δεν έχουν υποτύπους, ονομάζονται 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
Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα

Μάθε την έννοια Τύποι