Finora il sillabo non ha detto molto sui tipi, ma è chiaro che in Julia esistono:
julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')
julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}
Non abbiamo mai specificato i tipi, ma Julia li ha assegnati comunque.
types, che sono centrali nella sua progettazione.Type Inference.Il compilatore JIT esamina (tutto) il codice, vede come viene usata una variabile e ne inferisce un tipo predefinito adatto, compatibile con quell'uso.
Ci sono stati molti esempi, nei concetti precedenti, che costringono i tipi numerici ad uniformarsi, a cominciare dall'aritmetica più semplice.
julia> 2 + 1.3
3.3
Abbiamo sommato un numero intero (Int64) e un numero in virgola mobile (Float64) e abbiamo ottenuto un risultato Float64. Allo stesso modo:
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
Nel caso precedente, una tupla ha conservato il tipo di ogni elemento, mentre la conversione in un vettore li ha trasformati tutti in un Float64 uniforme.
Il compilatore Julia sa quali conversioni di tipo sono possibili: da intero a virgola mobile non c'è problema, da virgola mobile a intero si perde precisione e viene generato un InexactError.
La promozione dei tipi converte tutti i valori dell'espressione in un tipo comune, abbastanza versatile da essere compatibile con tutti gli input.
La stessa conversione si può fare esplicitamente con la funzione promote():
julia> promote(nums...)
(3.0, 4.1, 0.25)
Affidarsi all'inferenza dei tipi e alla promozione dei tipi va bene per risolvere semplici esercizi da tutorial, ma per programmi più grandi probabilmente avrai bisogno di un controllo più preciso.
Sulla maggior parte dei processori moderni, un intero ha come tipo predefinito Int64.
Nel concetto Numbers abbiamo visto che un valore può essere convertito in un tipo specifico diverso da quello predefinito.
julia> x = Int16(42)
42
julia> typeof(x)
Int16
Tuttavia, alla variabile x si può ancora assegnare un tipo diverso:
julia> x = "changed"
"changed"
julia> typeof(x)
String
Questa è la type instability, che è:
Invece, possiamo impostare il tipo di x usando l'operatore :::
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.
Ora y è, e sarà sempre, di tipo Int16.
Così il compilatore sa quanti byte riservare per y e può ottimizzare il resto del codice contando su un tipo stabile.
In questo, la variabile è più o meno simile a quelle di un linguaggio a tipizzazione statica come il C.
L'assegnazione dei tipi, descritta sopra, si usa a sinistra in un'assegnazione di variabile per vincolare il tipo di quella variabile.
Usare l'operatore :: con un valore, o con qualcosa che restituisce un valore, è in genere un'asserzione che il valore deve essere di quel tipo, altrimenti va generato un errore.
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
Spesso lo si usa con il valore restituito da una funzione, come ultimo semplice controllo che la funzione si sia comportata come previsto.
Nota che esiste una macro @assert per altre forme di asserzione.
Int64, Int16, String, Char: da dove «vengono» questi tipi.
In molti linguaggi orientati agli oggetti (OO), ogni tipo è una classe, le sottoclassi li dispongono in una gerarchia di classi, mentre i metodi delle classi ne definiscono il comportamento.
Java e Ruby sono esempi evidenti di questo schema, ma anche Python funziona così al suo interno.
Julia non ha classi.
La motivazione documentata è che le funzionalità OO interferiscono con il compilatore JIT e danneggiano le prestazioni a runtime.
Chissà, forse avevano letto anche questa citazione:
«La programmazione orientata agli oggetti è un'idea eccezionalmente cattiva, che poteva nascere solo in California.»
È attribuita a Edsger Dijkstra, un brillante informatico attivo per diversi decenni a partire dagli anni '50 (anche se non era generalmente noto per il suo ottimismo solare o la sua sottile diplomazia).
Eppure, guarda questo codice:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> supertype(Int16)
Signed
julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)
Qualche dettaglio in più:
Int16 è un tipo, e possiamo creare variabili di questo tipo.Int16 è un sottotipo di Signed, e la funzione supertype() ce lo mostra.Integer, Real e Number fino a Any in cima, e subtypes() ci elenca questo ramo della gerarchia.Tutti i rami terminano in Any, che ha la particolarità di essere supertipo di se stesso.
julia> supertypes(String)
(String, AbstractString, Any)
julia> supertype(Any)
Any
Quindi Julia non ha una gerarchia di class, ma ha una gerarchia di type.
Provare a mostrare l'intera gerarchia dà un albero enorme, impossibile da visualizzare davvero.
Guardarne alcune parti è una cosa discussa online.
Prima o poi proveremo a districare come funziona, ma prima c'è molto altro da esplorare.
Possiamo usare typeof() per verificare l'uguaglianza nel modo consueto.
julia> typeof(11)
Int64
julia> typeof(11) == Int64
true
julia> typeof(11) == Number
false
L'uguaglianza tra tipi deve essere esatta, perché questa forma di confronto non tiene conto della gerarchia dei tipi.
Più flessibilmente, isa ci dice se un valore ha lo stesso tipo di un termine di confronto, oppure un suo sottotipo.
Si può usare sia in forma infissa sia come funzione.
julia> 12 isa Int64
true
julia> 12 isa Number
true
julia> isa(12, Number)
true
julia> 12 isa String
false
Nota che isa si aspetta un value a sinistra, non un type.
Provare a confrontare due tipi in questo modo dà risultati inattesi.
L'operatore corretto è <:, che vedremo molto più spesso nei prossimi concetti.
julia> Int64 isa Number ## Don't do this!
false
julia> Int64 <: Number
true
Verificare i tipi può servire a controllare il flusso all'interno di una funzione, ma è relativamente raro nel Julia idiomatico.
Nel concetto Multiple Dispatch vedremo che spesso è più efficiente aggiungere tipi agli argomenti di una funzione e lasciare che sia il meccanismo di dispatch di Julia a gestire questa logica. Tuttavia, ci sono diversi altri concetti legati ai tipi di cui dobbiamo parlare prima di arrivare al Multiple Dispatch.
Abbiamo visto che la gerarchia dei tipi forma una struttura ad albero (nel senso dell'informatica, con la radice in cima).
Ogni elemento dell'albero è un node, e questi si dividono in categorie:
abstract.concrete.julia> subtypes(Integer) # an abstract type
3-element Vector{Any}:
Bool
Signed
Unsigned
julia> subtypes(Int64) # a concrete type
Type[]
Questa è una distinzione importante, perché solo i tipi concreti possono essere instantiated come variabili.
julia> a::Int16 = 42
42
julia> typeof(a)
Int16
julia> b::Integer = 42
42
julia> typeof(b)
Int64
Nota che provare a usare un tipo astratto non dà alcun messaggio di errore (in questo caso), ma il compilatore crea un tipo concreto appropriato: Int64 invece di Integer.
Almeno impedisce che alla variabile venga assegnato un valore non intero:
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.
L'assegnazione di tipo con un tipo astratto è quindi un vincolo sul tipo della variabile: qualunque sottotipo di (nel caso precedente) Integer.
È una cosa insolita nel mondo dei linguaggi di programmazione: più debole di un'assegnazione di tipo in C, più forte di un type hint nelle versioni recenti di Python.
Le funzioni isabstracttype() e isconcretetype() permettono di fare questi controlli.
Nota che non sono semplicemente la negazione l'una dell'altra: in un concetto successivo vedremo che alcuni tipi non sono né astratti né concreti.
julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)
julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)
# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)
Anche se Vector non è un tipo concreto, i suoi elementi lo sono.
La funzione eltype() (tipo degli elementi) estrae questo tipo:
julia> eltype([1, 2.3])
Float64