Ti

Tipi in Julia

1 esercizio

Informazioni su Tipi

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.

  1. Julia ha i types, che sono centrali nella sua progettazione.
  2. Julia di solito riesce a «indovinare» il tipo, usando la 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.

Promozione dei tipi

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)

Assegnazione dei tipi

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 è:

  • Molto comoda negli script piccoli.
  • Dannosa per le prestazioni e l'affidabilità nei programmi più grandi.

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.

Asserzione di tipo

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.

La gerarchia dei tipi

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.
  • Esiste una gerarchia di tipi che risale attraverso 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.

Verificare i tipi

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.

Tipi astratti e tipi concreti

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:

  1. I nodi che hanno sottotipi si chiamano abstract.
  2. I nodi foglia, senza sottotipi, si chiamano 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
Modifica tramite GitHub Il collegamento si apre in una nuova finestra o scheda

Impara Tipi