Lezione 4 di 7 · 20 min di lettura
Variabili e tipi
var, l'inferenza dei tipi, le costanti comptime e i tipi fondamentali di Mojo. Con un primo sguardo a SIMD, il segreto dei suoi numeri.
Un nome per un valore
In Mojo una variabile si dichiara con la parola chiave var:
def main():
var name = "Ada"
var year = 1843
var height = 1.65
var curious = True
print(name, year, height, curious)Ada 1843 1.65 TrueNon abbiamo scritto nessun tipo, eppure ognuna di queste variabili ne ha uno, deciso una volta per tutte a tempo di compilazione: name è una String, year un Int, height un Float64, curious un Bool. Il compilatore li ha dedotti dai valori iniziali: si chiama inferenza di tipo.
Se preferisci essere esplicito, o se vuoi un tipo diverso da quello che il compilatore sceglierebbe, puoi annotarlo dopo il nome, con i due punti:
var year: Int = 1843
var height: Float32 = 1.65Qui height è un decimale a 32 bit invece che a 64: con l’annotazione hai scelto tu.
Il tipo non cambia, mai
Una variabile Mojo può cambiare valore quante volte vuoi, ma il suo tipo resta quello della dichiarazione:
def main():
var count = 8
count = "nine"
print(count)/home/ada/learn-mojo/retype.mojo:3:13: error: cannot implicitly convert 'StringLiteral["nine"]' value to 'Int'
count = "nine"
^~~~~~
/home/ada/learn-mojo/.venv/bin/mojo: error: failed to parse the provided Mojo source moduleLeggi il messaggio con attenzione: count è un Int, e il compilatore ha cercato un modo per trasformare quel testo in un intero, senza trovarlo. Nota anche il tipo del testo: non String ma StringLiteral["nine"], il tipo speciale dei letterali scritti nel codice, che diventa una String quando finisce in una variabile.
Dichiarare prima, assegnare dopo
Puoi dichiarare una variabile senza darle subito un valore, purché indichi il tipo:
def main():
var total: Int
total = 40 + 2
print(total)42Il compilatore, però, controlla che tu non la usi prima di averle dato un valore:
def main():
var total: Int
print(total)/home/ada/learn-mojo/later.mojo:3:10: error: use of uninitialized value 'total'
print(total)
^
/home/ada/learn-mojo/later.mojo:2:9: note: 'total' declared here
var total: Int
^
/home/ada/learn-mojo/.venv/bin/mojo: error: failed to run the pass managerIn C leggere una variabile non inizializzata è lecito e restituisce quello che c’era in memoria: una fonte classica di bug imprevedibili. Mojo lo rende impossibile. La riga note: ti indica anche dove la variabile è stata dichiarata.
var è obbligatorio
Un’altra differenza con Python: una variabile nuova si introduce sempre con var.
def main():
x = 5
print(x)/home/ada/learn-mojo/implicit.mojo:2:5: error: implicit declaration of 'x' is not allowed; add 'var' to declare a new name
x = 5
^
var
/home/ada/learn-mojo/.venv/bin/mojo: error: failed to parse the provided Mojo source moduleIl compilatore ti mostra perfino cosa aggiungere e dove. Il motivo della regola è pratico: con var obbligatorio, un errore di battitura come totl = 3 (invece di total = 3) non crea in silenzio una variabile nuova, ma diventa un errore. Per la stessa ragione non puoi dichiarare due volte lo stesso nome nello stesso blocco:
def main():
var level = 1
var level = 2
print(level)/home/ada/learn-mojo/twice.mojo:3:9: error: invalid redefinition of 'level'
var level = 2
^
/home/ada/learn-mojo/twice.mojo:2:9: note: previous definition here
var level = 1
^
/home/ada/learn-mojo/.venv/bin/mojo: error: failed to parse the provided Mojo source modulePer cambiare il valore di una variabile esistente, si assegna e basta, senza var:
def main():
var level = 1
print("Level", level)
level = 2
print("Level", level)
level = level * 10
print("Level", level)Level 1
Level 2
Level 20level = level * 10 si legge da destra a sinistra: calcola level * 10 con il valore attuale, poi mettilo in level. In Mojo tutte le variabili sono modificabili: non esiste un let o un const per le variabili di una funzione.
comptime: le costanti
E se ti serve un valore che non deve cambiare, come la velocità della luce o il numero di secondi in un minuto? Si usa comptime:
comptime SPEED_OF_LIGHT = 299_792_458 # metres per second
comptime SECONDS_PER_MINUTE = 60
def main():
var light_minute = SPEED_OF_LIGHT * SECONDS_PER_MINUTE
print("In one minute light travels", light_minute, "metres")In one minute light travels 17987547480 metresUn valore comptime è calcolato durante la compilazione e diventa una costante nel programma finale: a runtime non occupa una variabile e non costa niente. Nota anche i trattini bassi in 299_792_458: servono solo agli occhi umani, il compilatore li ignora.
comptime è l’unico modo di avere un valore a livello di file, fuori dalle funzioni. Le variabili globali, semplicemente, non esistono:
var counter = 0
def main():
print(counter)/home/ada/learn-mojo/global.mojo:1:1: error: global variables are not supported; move this into a function body or use 'comptime' to declare a constant
var counter = 0
^
/home/ada/learn-mojo/global.mojo:5:11: error: use of unknown declaration 'counter'
print(counter)
^~~~~~~
/home/ada/learn-mojo/.venv/bin/mojo: error: failed to parse the provided Mojo source moduleE naturalmente una costante non si può riassegnare: SPEED_OF_LIGHT = 3 dentro main produce error: expression must be mutable in assignment.
Quiz
Quale di questi programmi compila?
I tipi numerici: interi
Int è l’intero di default: un numero con segno grande quanto una parola macchina, cioè 64 bit su qualsiasi computer moderno. Va da circa −9,2 miliardi di miliardi a +9,2 miliardi di miliardi, abbastanza per quasi tutto.
Quando serve controllare esattamente quanta memoria occupa ogni numero (e nel calcolo numerico serve spesso: un array di un miliardo di valori a 8 bit occupa 1 GB, a 64 bit ne occupa 8), Mojo offre gli interi a larghezza fissa: Int8, Int16, Int32, Int64, Int128, Int256 con segno e UInt8 … UInt256 senza segno. Ognuno ha un intervallo limitato, e cosa succede quando lo superi è importante:
def main():
var small: Int8 = 127
small += 1
print(small)
var byte: UInt8 = 255
byte += 1
print(byte)
var big = 9_223_372_036_854_775_807
big += 1
print(big)-128
0
-9223372036854775808(+= 1 significa “aumenta di 1”: ne parliamo nella prossima lezione.) Nessun errore, nessun avviso: il valore trabocca e ricomincia dall’altra parte dell’intervallo. Succede anche con Int, solo molto più in là. È il comportamento del processore stesso, ed è la ragione per cui gli interi di Mojo sono veloci quanto quelli del C: nessun controllo a ogni operazione.
Dettagli nerd Perché 127 + 1 fa −128? (il complemento a due)
Un Int8 è fatto di 8 bit, quindi può assumere 2⁸ = 256 configurazioni. I processori rappresentano gli interi con segno in complemento a due: le configurazioni da 00000000 a 01111111 sono i numeri da 0 a 127, quelle da 10000000 a 11111111 sono i numeri da −128 a −1. Il primo bit, in pratica, “vale” −128 invece di +128.
127 è 01111111. Aggiungendo 1 ottieni 10000000, che in complemento a due è −128. Il processore non ha fatto niente di strano: ha sommato in binario come sempre, ed è l’interpretazione dei bit a produrre il salto. Il vantaggio del complemento a due è che somma e sottrazione funzionano con lo stesso circuito per numeri positivi e negativi.
I tipi numerici: decimali
Float64 è il numero decimale di default: 64 bit in formato IEEE 754, circa 15-16 cifre significative. È il float di Python e il double del C. Esistono anche Float32 (meno preciso, metà della memoria), Float16 e BFloat16, e formati a 8 e 4 bit usati sulle GPU per l’intelligenza artificiale:
def main():
var third64 = 1.0 / 3.0
var third32: Float32 = 1.0 / 3.0
print(third64)
print(third32)0.3333333333333333
0.33333334Con 32 bit le cifre affidabili sono circa 7, e si vede. Nell’intelligenza artificiale, però, i formati piccoli sono preziosi: un modello con miliardi di parametri a 16 o 8 bit occupa da due a otto volte meno memoria che a 64 bit, e trasferire meno byte (dalla memoria al processore, o alla GPU) è spesso il collo di bottiglia. Il B di BFloat16, per curiosità, sta per Brain, da Google Brain, che lo ha inventato per i suoi acceleratori.
Dettagli nerd Come si scrive 1/3 con solo 0 e 1? (IEEE 754)
Un numero IEEE 754 è una specie di notazione scientifica in base 2: un bit di segno, un esponente e una mantissa (le cifre significative). In un Float64 sono 1 + 11 + 52 bit; in un Float32 1 + 8 + 23; in un BFloat16 1 + 8 + 7, lo stesso esponente di Float32 (quindi la stessa ampiezza di valori) con molta meno precisione.
1/3 in base 2 è 0.010101... all’infinito, come in base 10 è 0.333...: con un numero finito di bit bisogna tagliare, e il risultato è il valore rappresentabile più vicino. Quando print mostra 0.3333333333333333, sta scegliendo la più breve sequenza di cifre decimali che identifica esattamente quel valore binario. Lo standard prevede anche valori speciali: inf, -inf e nan (not a number), che incontreremo presto.
Bool e String
Bool ha due soli valori, True e False, con la maiuscola come in Python. String è il testo, codificato in UTF-8: ha molte sorprese interessanti, e le vedremo per bene nella lezione sulla stampa.
Il segreto dei numeri di Mojo: SIMD
C’è un dettaglio della libreria standard che spiega molto del carattere di Mojo. Float32, Int8, UInt64 e quasi tutti gli altri tipi numerici non sono tipi “primitivi”: sono tutti casi particolari di un unico tipo, SIMD, un vettore di numeri che il processore sa elaborare in blocco con una sola istruzione. Uno Float32 è un SIMD di tipo float32 con un elemento. Ma nessuno ti vieta di chiederne quattro:
def main():
var prices = SIMD[DType.float32, 4](1.5, 2.0, 3.25, 4.0)
var quantities = SIMD[DType.float32, 4](2, 1, 4, 3)
var totals = prices * quantities
print(totals)
print(totals.reduce_add())[3.0, 2.0, 13.0, 12.0]
30.0prices * quantities moltiplica le quattro coppie di numeri insieme, elemento per elemento, e reduce_add() somma gli elementi del vettore. Tra le parentesi quadre ci sono due parametri del tipo, noti a tempo di compilazione: il tipo degli elementi (DType.float32) e quanti sono (4, sempre una potenza di due). Per ora prendila come un’anteprima: ai vettori SIMD dedicheremo un modulo intero, perché sono uno dei motivi per cui il calcolo numerico in Mojo è così veloce.
Dettagli nerd Cos'è SIMD, esattamente?
SIMD sta per Single Instruction, Multiple Data: una sola istruzione che opera su più dati contemporaneamente. I processori moderni hanno registri vettoriali larghi 128, 256 o 512 bit: in un registro da 256 bit (le istruzioni AVX2, che Mojo richiede sui processori x86) stanno 8 Float32 o 32 Int8. Un’istruzione come vmulps li moltiplica tutti in un colpo solo, nello stesso tempo che servirebbe per una moltiplicazione singola.
Nella maggior parte dei linguaggi usare queste istruzioni richiede funzioni speciali del compilatore (gli intrinsics) o la speranza che il compilatore le usi da solo. In Mojo sono il fondamento stesso dei tipi numerici.
Conversioni: sempre esplicite
Mojo non converte mai da solo un tipo numerico in un altro. Nemmeno da Int a Float64, che in molti linguaggi è automatico:
def main():
var items = 3
var price = 1.5
print(items * price)/home/ada/learn-mojo/mix.mojo:4:19: error: invalid call to '__mul__': value passed to 'rhs' cannot be converted from 'Float64' to 'Int'
print(items * price)
~~~~~~~~^~~~~
Imported from /home/ada/learn-mojo/mix.mojo:1:
Mojo/stdlib/std/simd.mojo:1102:1: note: function declared here
def __mul__(self, rhs: Self) -> Self # note - synthetic signature
^
/home/ada/learn-mojo/.venv/bin/mojo: error: failed to parse the provided Mojo source moduleIl messaggio è più tecnico dei precedenti, ma dice una cosa semplice: * tra un Int e qualcosa vuole un altro Int (è la funzione __mul__ di Int, e le note ti mostrano dove è definita nella libreria standard). Non esistendo un modo sicuro di trasformare 1.5 in un intero, il compilatore si ferma. La conversione si scrive chiamando il tipo di destinazione come una funzione:
def main() raises:
var items = 7
var price = 2.5
var total = Float64(items) * price
print(total)
print(Int(total))
print("Items: " + String(items))
var parsed = Int("42")
print(parsed + 1)17.5
17
Items: 7
43Float64(items)trasforma l’intero in decimale.Int(total)fa il contrario, troncando la parte decimale (verso lo zero: convar x = -7.9,Int(x)fa-7).String(items)trasforma un numero in testo, per poterlo concatenare con+.Int("42")fa il contrario: legge un numero da un testo.
Quest’ultima può fallire: cosa dovrebbe restituire Int("12a")? In Mojo una funzione che può fallire si dice che solleva un errore (raises), e il compilatore pretende che tu te ne occupi. La soluzione più semplice, per ora, è dichiarare che anche main può sollevare errori, scrivendo raises prima dei due punti: def main() raises:. Se togli raises, il compilatore protesta con cannot call function that may raise in a context that cannot raise. E se il testo non è un numero, il programma si ferma con un messaggio:
def main() raises:
var n = Int("12a")
print(n)stack trace was not collected. Enable stack trace collection with environment variable `MODULAR_DEBUG=stack-trace-on-error`
Unhandled exception caught during execution: String is not convertible to integer with base 10: '12a'
/home/ada/learn-mojo/.venv/bin/mojo: error: execution exited with a non-zero result: 1Nota la differenza con gli errori visti finora: questo non è un errore di compilazione, il programma è partito ed è l’esecuzione a essersi fermata. Come gestire gli errori senza far terminare il programma lo vedremo in un modulo dedicato.
Quiz
Hai var n = 10 e var rate = 0.25. Quale espressione calcola correttamente 2.5?
Mettiamo tutto insieme
Esercizio · sul tuo computer
La scheda del pianeta
Crea planet.mojo con:
- una costante
comptime SECONDS_PER_HOUR = 3600; - dentro
main, le variabiliname("Mars"),moons(2),gravity(3.71),habitable(False); - una variabile
day_secondscalcolata a partire daSECONDS_PER_HOUR: un giorno marziano dura 24 ore, 39 minuti e 35 secondi.
Il programma deve stampare esattamente:
Planet: Mars
Moons: 2
Gravity: 3.71 m/s2
Day: 88775 s
Habitable: FalseSuggerimento: print separa gli argomenti con uno spazio, quindi print("Moons:", moons) basta per la seconda riga.
Mostra una soluzione (prima prova da solo!)
comptime SECONDS_PER_HOUR = 3600
def main():
var name = "Mars"
var moons = 2
var gravity = 3.71
var habitable = False
var day_seconds = 24 * SECONDS_PER_HOUR + 39 * 60 + 35
print("Planet:", name)
print("Moons:", moons)
print("Gravity:", gravity, "m/s2")
print("Day:", day_seconds, "s")
print("Habitable:", habitable)Ricapitolando
var nome = valoredichiara una variabile; il tipo è dedotto dal valore o annotato convar nome: Tipo = valore.- Il tipo di una variabile non cambia mai; il valore sì.
varè obbligatorio e un nome non si ridichiara nello stesso blocco. - Si può dichiarare senza valore (
var total: Int) ma non usare prima di assegnare. comptime NOME = valoredefinisce una costante calcolata in compilazione; le variabili globali non esistono.Int(64 bit) e gli interi a larghezza fissaInt8…UInt256traboccano senza avvisare.Float64è il decimale di default;Float32,Float16,BFloat16risparmiano memoria a scapito della precisione.- Tutti i numeri sono vettori
SIMD:SIMD[DType.float32, 4]ne tiene quattro e li elabora insieme. - Le conversioni sono sempre esplicite:
Float64(n),Int(x),String(n),Int("42")(che può sollevare errori:def main() raises:).
Nella prossima lezione facciamo i conti: operatori, divisioni che non si comportano come in Python e numeri calcolati con precisione infinita.