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:

arith.mojo
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)
output
22 12 85
3 3 2
289

Somma, 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:

negative.mojo
def main():
    var a = -7
    var b = 2
    print(a / b, a // b, a % b)
output
-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):

float_ops.mojo
def main():
    var x = 7.0
    var y = 2.0
    print(x / y, x // y, x % y, x**y)
output
3.5 3.0 1.0 49.0

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

float_zero.mojo
def main():
    var zero = 0.0
    print(1.0 / zero, -1.0 / zero, zero / zero)
output
inf -inf nan

inf è 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.

divide.mojo
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:

terminale
mojo divide.mojo a b c d
output
Dividing by 4
25

Senza argomenti, il divisore è zero:

terminale
mojo divide.mojo
output
Dividing 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:

literals.mojo
def main():
    print(0.1 + 0.2)
    var a = 0.1
    var b = 0.2
    print(a + b)
    print(a + b == 0.3)
output
0.3
0.30000000000000004
False

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

literal_division.mojo
def main():
    print(7 / 2)
    var half = 7 / 2
    print(half)
    var a = 7
    var b = 2
    print(a / b)
output
3.5
3.5
3

Tra 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.

compare.mojo
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")
output
Fever: False
Normal: True
True True

Le 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.

logic.mojo
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)
output
True
False

and 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.

ternary.mojo
def main():
    var score = 72
    var result = "pass" if score >= 60 else "fail"
    print(result)
output
pass

Quiz

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 -=, *=, /=, //=, %=, **=:

compound.mojo
def main():
    var energy = 100
    energy -= 30
    energy *= 2
    energy //= 3
    print(energy)
    energy **= 2
    print(energy)
output
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:

precedence.mojo
def main():
    var a = 2
    print(2 + 3 * 4**2)
    print((2 + 3) * 4)
    print(-(a**2), (-a) ** 2)
output
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 format lo 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:

laptop.mojo
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)
output
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:

output
Total: 200000 seconds
Days: 2
Hours: 7
Minutes: 33
Seconds: 20

Usa 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!)
duration.mojo
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, -inf o nan; 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) stampa 3.5 e print(0.1 + 0.2) stampa 0.3. Le variabili seguono le regole del loro tipo (tra Float64, 0.1 + 0.2 non è 0.3: usa isclose).
  • Confronti == != < <= > >=, concatenabili (a < x < b); logica con and, 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.