Lezione 5 di 7 · 18 min di lettura
Operatori
Aritmetica tra interi e decimali, due divisioni diverse, la divisione per zero, i letterali calcolati con precisione infinita, confronti, logica e precedenza.
Aritmetica tra interi
Gli operatori di Mojo sono quelli di Python, con la stessa precedenza e la stessa associatività. Tra due Int:
def main():
var a = 17
var b = 5
print(a + b, a - b, a * b)
print(a / b, a // b, a % b)
print(a**2)22 12 85
3 3 2
289Somma, sottrazione e moltiplicazione non hanno sorprese. ** è la potenza (il formattatore la scrive senza spazi quando gli operandi sono semplici, come a**2). % è il resto della divisione. E poi c’è la riga centrale, che a chi viene da Python fa alzare un sopracciglio: a / b fa 3, non 3.4.
Due divisioni
Tra due valori Int, / produce un Int: il risultato viene troncato verso lo zero. // è invece la divisione con arrotondamento verso il basso (floor division). Con numeri positivi coincidono; con i negativi, no:
def main():
var a = -7
var b = 2
print(a / b, a // b, a % b)-3 -4 1−7 / 2 fa −3,5: / tronca verso lo zero e dà −3, // arrotonda verso meno infinito e dà −4. Il resto % segue la regola di //, come in Python: vale sempre a == b * (a // b) + a % b, quindi −7 % 2 fa 1 (perché 2 × −4 + 1 = −7).
Tra decimali, invece, / è la divisione vera, e anche // e % funzionano (e restituiscono decimali):
def main():
var x = 7.0
var y = 2.0
print(x / y, x // y, x % y, x**y)3.5 3.0 1.0 49.0Ricorda la lezione precedente: gli operatori vogliono operandi dello stesso tipo. Int con Int, Float64 con Float64; per mescolarli si converte esplicitamente.
Quiz
Con var n = 9, quanto vale n / 2 * 2?
Dividere per zero
Tra decimali, lo standard IEEE 754 ha una risposta per tutto:
def main():
var zero = 0.0
print(1.0 / zero, -1.0 / zero, zero / zero)inf -inf naninf è infinito, nan (not a number) è il risultato di un’operazione senza senso come 0/0. Il programma continua a girare, e questi valori si propagano nei calcoli successivi: un nan che compare in fondo a una lunga simulazione è spesso il segno di una divisione per zero avvenuta molto prima.
Tra interi, invece, non c’è nessun valore sensato da restituire. Proviamo con un divisore che arriva dall’esterno, così il compilatore non può conoscerlo in anticipo: argv() restituisce gli argomenti scritti dopo il nome del programma, e len(argv()) - 1 è quanti sono.
from std.sys import argv
def main():
var divisor = len(argv()) - 1
print("Dividing by", divisor)
print(100 / divisor)(La prima riga importa argv dal modulo sys della libreria standard: degli import parleremo più avanti.) Con quattro argomenti tutto va bene:
mojo divide.mojo a b c dDividing by 4
25Senza argomenti, il divisore è zero:
mojo divide.mojoDividing by 0
Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var `LLVM_SYMBOLIZER_PATH` to point to it):
0 libKGENCompilerRTShared.so 0x000073847c1c221e
...
/home/ada/learn-mojo/.venv/bin/mojo: error: execution crashed
To get a symbolicated stack trace, compile your program using `mojo build` with debug info enabled (e.g., `-debug-level=line-tables`) and execute it separately.Il programma va in crash: è il processore stesso a rifiutare la divisione intera per zero, e il sistema operativo termina il processo. Gli indirizzi esadecimali cambiano a ogni esecuzione.
Precisione infinita, a tempo di compilazione
Ecco una stranezza che vale la pena conoscere subito. Il classico esempio dei decimali:
def main():
print(0.1 + 0.2)
var a = 0.1
var b = 0.2
print(a + b)
print(a + b == 0.3)0.3
0.30000000000000004
FalseLa prima riga stampa 0.3, la seconda il famigerato 0.30000000000000004. Stesse operazioni, risultati diversi. Il motivo: i letterali numerici scritti nel codice (0.1, 42, 1e9) hanno tipi speciali, FloatLiteral e IntLiteral, che esistono solo durante la compilazione e hanno precisione arbitraria. 0.1 + 0.2 tra letterali viene calcolato dal compilatore in modo esatto, e solo il risultato finale diventa un Float64. Appena i valori finiscono in variabili, diventano Float64 a 64 bit e l’aritmetica è quella del processore, con i suoi arrotondamenti.
Lo stesso vale per la divisione, ed è la trappola più sottile di questa lezione:
def main():
print(7 / 2)
var half = 7 / 2
print(half)
var a = 7
var b = 2
print(a / b)3.5
3.5
3Tra due letterali interi, / è la divisione esatta della matematica, e il risultato è un letterale decimale: 7 / 2 vale 3,5, e var half = 7 / 2 crea un Float64. Tra due variabili Int, / è la divisione intera del processore. Stesso simbolo, stessi numeri, tipi diversi.
In pratica: le espressioni fatte solo di letterali e costanti comptime sono calcolate in modo esatto dal compilatore e sono gratuite a runtime; appena entra in gioco una variabile, valgono le regole del suo tipo: divisione intera per gli Int, IEEE 754 per i Float64.
Dettagli nerd Perché 0,1 + 0,2 non fa 0,3?
0,1 in base 2 è un numero periodico: 0.000110011001100... all’infinito, come 1/3 in base 10. Un Float64 ne tiene 52 cifre binarie di mantissa e arrotonda il resto, quindi la variabile a contiene in realtà 0,1000000000000000055511151231257827… Lo stesso vale per 0,2. La somma dei due valori arrotondati cade appena sopra il Float64 più vicino a 0,3, e print mostra esattamente quel valore.
Non è un difetto di Mojo: Python, JavaScript, C e qualsiasi linguaggio che usa l’hardware IEEE 754 si comportano allo stesso modo. Per confrontare decimali si usa una tolleranza: isclose(a + b, 0.3), da importare con from std.math import isclose, restituisce True.
Confronti
Gli operatori di confronto sono ==, !=, <, <=, >, >= e restituiscono un Bool. Come in Python, si possono concatenare: 36.0 <= t < 37.5 significa 36.0 <= t and t < 37.5.
def main():
var t = 36.6
var fever = t >= 37.5
print("Fever:", fever)
print("Normal:", 36.0 <= t < 37.5)
print("apple" < "banana", "Zebra" < "ant")Fever: False
Normal: True
True TrueLe stringhe si confrontano in ordine lessicografico, carattere per carattere secondo il codice Unicode: per questo "Zebra" < "ant" è vero, dato che tutte le maiuscole vengono prima delle minuscole.
Logica
Gli operatori logici sono parole, come in Python: and, or, not.
def main():
var age = 20
var has_ticket = True
var can_enter = age >= 18 and has_ticket
print(can_enter)
print(not can_enter or age > 65)True
Falseand e or sono a cortocircuito: se il lato sinistro basta a decidere il risultato (False and ..., True or ...), il lato destro non viene nemmeno valutato. È utile quando il lato destro è costoso, o quando ha senso solo se il sinistro è vero.
Mojo ha anche l’espressione condizionale, che sceglie tra due valori: valore_se_vero if condizione else valore_se_falso.
def main():
var score = 72
var result = "pass" if score >= 60 else "fail"
print(result)passQuiz
Con var a = 5 e var b = 10, quanto vale a * 2 == b and b / a == 2?
Assegnazione composta
Per aggiornare una variabile a partire dal suo valore c’è la forma abbreviata +=, e con lei -=, *=, /=, //=, %=, **=:
def main():
var energy = 100
energy -= 30
energy *= 2
energy //= 3
print(energy)
energy **= 2
print(energy)46
2116(100 − 30 = 70, per 2 fa 140, diviso 3 con arrotondamento verso il basso fa 46, al quadrato 2116.) x += 1 significa esattamente x = x + 1. Non esistono invece x++ e x--.
Precedenza
Dalla più forte alla più debole: chiamate di funzione, **, il segno meno davanti a un numero, * / // %, + -, gli operatori sui bit, i confronti, not, and, or, e per ultima l’espressione condizionale if/else. Due dettagli che sorprendono:
def main():
var a = 2
print(2 + 3 * 4**2)
print((2 + 3) * 4)
print(-(a**2), (-a) ** 2)50
20
-4 4**lega più forte del segno meno:-a**2è-(a**2), cioè −4, come in matematica e in Python. Talmente facile da fraintendere che, se scrivi-a**2,mojo formatlo riscrive da solo come-(a**2).**è associativo a destra:2**3**2è2**(3**2), cioè 512.
Le parentesi tonde raggruppano come ci si aspetta, e non costano nulla a runtime. Quando la precedenza non è ovvia a prima vista, mettile.
Un programma vero
Quanta energia consuma una sessione di lavoro al portatile? Con le costanti in cima e qualche operatore:
comptime WATTS = 65
comptime MINUTES = 215
def main():
var hours = MINUTES // 60
var minutes = MINUTES % 60
var kwh = Float64(WATTS * MINUTES) / 60.0 / 1000.0
print("Session:", hours, "h", minutes, "min")
print("Energy:", kwh, "kWh")
print("Above 0.2 kWh:", kwh > 0.2)Session: 3 h 35 min
Energy: 0.23291666666666666 kWh
Above 0.2 kWh: True// e % insieme spezzano i minuti in ore e minuti: è un’accoppiata che userai spessissimo. Per l’energia, watt per minuti fa wattminuti, diviso 60 fa wattora, diviso 1000 fa kilowattora. Quel 0.23291666666666666 è corretto ma illeggibile: nella prossima lezione impariamo ad arrotondarlo e a impaginarlo.
Esercizio · sul tuo computer
Quanto dura?
Crea duration.mojo che, partendo da var total = 200000 secondi, calcoli quanti giorni, ore, minuti e secondi sono, e stampi:
Total: 200000 seconds
Days: 2
Hours: 7
Minutes: 33
Seconds: 20Usa solo // e %. Suggerimento: un giorno ha 86400 secondi; quello che avanza dopo aver tolto i giorni si divide in ore, e così via. Puoi riusare una variabile rest aggiornandola con %=.
Mostra una soluzione (prima prova da solo!)
comptime SECONDS_PER_DAY = 86400
comptime SECONDS_PER_HOUR = 3600
def main():
var total = 200000
var days = total // SECONDS_PER_DAY
var rest = total % SECONDS_PER_DAY
var hours = rest // SECONDS_PER_HOUR
rest %= SECONDS_PER_HOUR
var minutes = rest // 60
var seconds = rest % 60
print("Total:", total, "seconds")
print("Days:", days)
print("Hours:", hours)
print("Minutes:", minutes)
print("Seconds:", seconds)Ricapitolando
+ - * ** %come in Python; gli operandi devono avere lo stesso tipo.- Tra variabili
Int,/tronca verso lo zero (7/2 dà 3, −7/2 dà −3);//arrotonda verso il basso (−7//2 dà −4);%segue//. - Decimali divisi per zero danno
inf,-infonan; interi divisi per zero mandano in crash il programma (o peggio, se il compilatore vede lo zero). - I letterali sono calcolati con precisione arbitraria durante la compilazione:
print(7 / 2)stampa3.5eprint(0.1 + 0.2)stampa0.3. Le variabili seguono le regole del loro tipo (traFloat64,0.1 + 0.2non è0.3: usaisclose). - Confronti
== != < <= > >=, concatenabili (a < x < b); logica conand,or,not;a if cond else b. +=,-=,*=,//=,%=,**=aggiornano una variabile; niente++.**batte il meno unario ed è associativo a destra; in caso di dubbio, parentesi.
Nella prossima lezione: stringhe, t-string, arrotondamenti e impaginazione, per trasformare i numeri in un output che si legge.