Lezione 5 di 7 · 18 min di lettura

Operatori

Fare i conti con Int e Float (che non si mescolano), confrontare valori, combinare condizioni. E una sorpresa sulla divisione per zero.

Il computer è una calcolatrice molto pignola

Le quattro operazioni funzionano come te le aspetti, più una quinta: il resto.

src/ints.gleam
import gleam/int
import gleam/io

pub fn main() -> Nil {
  let a = 17
  let b = 5
  io.println("a + b = " <> int.to_string(a + b))
  io.println("a - b = " <> int.to_string(a - b))
  io.println("a * b = " <> int.to_string(a * b))
  io.println("a / b = " <> int.to_string(a / b))
  io.println("a % b = " <> int.to_string(a % b))
}
output
a + b = 22
a - b = 12
a * b = 85
a / b = 3
a % b = 2

Somma, sottrazione e moltiplicazione sono quelle di sempre. Le ultime due meritano un paragrafo ciascuna.

La divisione tra interi

17 diviso 5 fa 3,4. Ma a / b ha stampato 3. Non è un errore: quando dividi due Int, il risultato è un Int, e la parte decimale viene tagliata via (non arrotondata: tagliata). 9 / 2 fa 4, non 5; 1 / 3 fa 0.

Con i numeri negativi il taglio avviene verso lo zero: -7 / 2 fa -3, non -4.

Quiz

Quanto vale 9 / 2 * 2?

Il resto: %

L’operatore % restituisce il resto della divisione intera: 17 diviso 5 fa 3 con il resto di 2, quindi 17 % 5 vale 2. Sembra un’operazione da scuola elementare, ma è uno degli attrezzi più usati in programmazione:

  • n % 2 vale 0 se n è pari, 1 se è dispari;
  • seconds % 60 sono i secondi “avanzati” dopo aver contato i minuti interi;
  • cents % 100 sono i centesimi dopo aver tolto gli euro interi.

Insieme, / e % spezzano un numero in unità più grandi e avanzo: 137 minuti sono 137 / 60 = 2 ore e 137 % 60 = 17 minuti.

La sorpresa: dividere per zero

Cosa fa 10 / 0? In matematica non ha senso, e in quasi tutti i linguaggi il programma si ferma con un errore. In Gleam, invece:

src/zero.gleam
import gleam/io

pub fn main() -> Nil {
  echo 10 / 0
  echo 10 % 0
  echo 3.0 /. 0.0
  io.println("Still alive")
}
output
src/zero.gleam:4
0
src/zero.gleam:5
0
src/zero.gleam:6
0.0
Still alive

In Gleam, dividere per zero dà zero. La spiegazione sta nelle regole del linguaggio: Gleam non ha eccezioni, quindi non può “lanciare un errore”, e la BEAM non ha un valore “infinito” da restituire. L’unica scelta rimasta è restituire zero, e la documentazione ufficiale lo dice chiaramente.

I Float hanno i loro operatori

Ed ecco una particolarità di Gleam che all’inizio fa storcere il naso: gli operatori sui Float sono diversi da quelli sugli Int. Hanno un punto in più.

OperazioneCon IntCon Float
somma++.
sottrazione--.
moltiplicazione**.
divisione//.

7.0 /. 2.0 fa 3.5, come ti aspetti. Ma se provi a mescolare i tipi, il compilatore ti ferma e ti dice cosa usare:

output
error: Type mismatch
  ┌─ /home/ada/learn-gleam/hello/src/hello.gleam:5:23
  │
5 │   let total = price * 1.5
  │                       ^^^

The * operator expects arguments of this type:

    Int

But this argument has this type:

    Float

Hint: The *. operator can be used with Floats

Qui price era un Int, e 1.5 un Float: * vuole due Int, *. vuole due Float, e non esiste un operatore che accetti un po’ e un po’. Se hai un Int e ti serve come Float, lo converti tu con int.to_float. Per il viaggio inverso, da Float a Int, devi decidere come perdere i decimali:

FunzioneCosa faEsempio
int.to_floatda Int a Floatint.to_float(3) → 3.0
float.roundarrotonda all’Int più vicinofloat.round(2.5) → 3
float.truncatetaglia i decimali, dà un Intfloat.truncate(2.9) → 2
float.floorarrotonda per difetto, resta Floatfloat.floor(2.9) → 2.0
float.ceilingarrotonda per eccesso, resta Floatfloat.ceiling(2.1) → 3.0
src/floats.gleam
import gleam/float
import gleam/int
import gleam/io

pub fn main() -> Nil {
  let price = 2.5
  let quantity = 3
  let total = price *. int.to_float(quantity)
  io.println("Total: " <> float.to_string(total))
}
output
Total: 7.5

C’è poi un dettaglio che riguarda i decimali di ogni linguaggio, non solo di Gleam:

src/tenths.gleam
import gleam/io

pub fn main() -> Nil {
  echo 0.1 +. 0.2
  io.println("Done")
}
output
src/tenths.gleam:4
0.30000000000000004
Done

Non è un bug di Gleam: è la conseguenza del modo in cui i computer memorizzano i decimali, raccontato nei dettagli nerd della lezione precedente (“Come si scrive 3,14 con solo 0 e 1?”). 0,1 e 0,2 non sono rappresentabili esattamente in binario, e i due piccoli errori sommati diventano visibili. Per questo i soldi, in un programma serio, non si tengono mai in Float: si contano in centesimi, con gli Int. Lo faremo tra poco.

Niente x += 1

Molti linguaggi hanno scorciatoie per modificare una variabile: x += 5, x++. In Gleam non ci sono, per il motivo che conosci: niente si modifica. Quando ti serve un valore aggiornato, crei un nuovo let:

gleam
  let score = 10
  let score = score + 5

Confronti: domande con risposta sì o no

Un confronto è un’espressione che fa una domanda e restituisce un Bool: True o False.

OperatoreDomandaCon Float
<è minore?<.
>è maggiore?>.
<=è minore o uguale?<=.
>=è maggiore o uguale?>=.
==è uguale?==
!=è diverso?!=

Anche qui i Float hanno la loro versione con il punto, e se ti dimentichi il compilatore te lo ricorda con un Use >. instead. Fanno eccezione == e !=, che funzionano con qualsiasi tipo: numeri, stringhe, booleani. L’unica condizione è che i due lati siano dello stesso tipo: 1 == 1.0 non compila, perché un Int e un Float non sono mai “uguali” per Gleam, sono proprio cose diverse.

src/compare.gleam
import gleam/io

pub fn main() -> Nil {
  let age = 17
  let height = 1.72
  let name = "gleam"
  echo age >= 18
  echo height >. 1.7
  echo name == "Gleam"
  echo age != 18
  io.println("Done")
}
output
src/compare.gleam:7
False
src/compare.gleam:8
True
src/compare.gleam:9
False
src/compare.gleam:10
True
Done

Nota name == "Gleam": il confronto tra stringhe distingue maiuscole e minuscole, quindi "gleam" e "Gleam" sono diverse.

Logica: combinare le domande

Spesso una domanda sola non basta. Per combinarle ci sono tre operatori logici:

OperatoreNomeÈ True quando…
&&e (and)entrambi i lati sono True
\|\|o (or)almeno uno dei due lati è True
!non (not)il valore è False (lo capovolge)
gleam
  let is_teenager = age >= 13 && age <= 19
  let gets_discount = age < 14 || age > 65
  let is_adult = !is_teenager && age > 19

Quiz

Con let a = 5 e let b = 10, quanto vale a * 2 == b && b / a == 2?

Precedenza: chi va per primo

Come in matematica, 2 + 3 * 4 fa 14, non 20: la moltiplicazione ha la precedenza sulla somma. L’ordine completo, da chi va per primo:

  1. ! e il meno davanti a un numero (-x);
  2. *, /, % (e le versioni con il punto);
  3. +, - (e le versioni con il punto);
  4. |>, che incontreremo nella prossima lezione;
  5. <> per attaccare le stringhe;
  6. i confronti di grandezza <, >, <=, >=;
  7. i confronti di uguaglianza ==, !=;
  8. &&;
  9. ||.

Non serve impararla a memoria: è costruita perché le espressioni “normali” si leggano come ti aspetti. a + 1 > b && ok è { a + 1 } > b, e il risultato messo in && con ok.

E se vuoi cambiare l’ordine? In matematica si usano le parentesi tonde. In Gleam, sorpresa: si usano le graffe.

gleam
  let result = { 2 + 3 } * 4

Ora result vale 20. Se provi con le tonde, il compilatore ti corregge con garbo:

output
error: Syntax error
  ┌─ /home/ada/learn-gleam/hello/src/hello.gleam:2:11
  │
2 │   let x = (1 + 2) * 3
  │           ^ This parenthesis cannot be understood here

Hint: To group expressions in Gleam, use "{" and "}"; tuples are created
with `#(` and `)`.

Le graffe non sono una stranezza a caso: in Gleam { ... } è un blocco, un gruppo di espressioni che vale quanto la sua ultima espressione. Proprio come il corpo di una funzione. { 2 + 3 } è semplicemente un blocco che contiene una sola espressione, e vale 5.

Dettagli nerd Perché le tonde servono ad altro?

In Gleam le parentesi tonde hanno già due lavori: racchiudono gli argomenti delle funzioni, come in io.println("hi"), e, precedute da #, costruiscono le tuple, gruppi di valori come #(1, "one") che incontreremo più avanti. Usarle anche per raggruppare i calcoli renderebbe il codice ambiguo da leggere, per le persone e per il compilatore. Con le graffe, ogni simbolo ha un solo significato.

Un programma vero

Mettiamo insieme tutto quello che sappiamo per calcolare un resto, con i soldi contati in centesimi:

src/change.gleam
import gleam/bool
import gleam/int
import gleam/io

pub fn main() -> Nil {
  let price = 1370
  let paid = 2000
  let change = paid - price
  let euros = change / 100
  let cents = change % 100

  io.println("Change: " <> int.to_string(change) <> " cents")
  io.println(
    "That is "
    <> int.to_string(euros)
    <> " euros and "
    <> int.to_string(cents)
    <> " cents",
  )
  io.println("Enough for a coffee: " <> bool.to_string(change >= 120))
}
output
Change: 630 cents
That is 6 euros and 30 cents
Enough for a coffee: True

Nota come gleam format ha spezzato la stringa lunga: una riga per pezzo, con i <> all’inizio. E l’ultimo argomento di io.println ha una virgola in fondo, che Gleam permette (e il formattatore aggiunge) quando gli argomenti vanno a capo.

Esercizio · sul tuo computer

Quanto dura?

Nel progetto exercises crea src/duration.gleam. Parti da un totale di 7384 secondi e stampa esattamente:

output
Total: 7384 seconds
Time: 2 h 3 min 4 s
More than 2 hours: True

Suggerimenti: le ore sono i secondi diviso 3600. Quello che avanza (% 3600) va diviso per 60 per avere i minuti, e quello che avanza ancora (% 60) sono i secondi. L’ultima riga è un confronto convertito in testo con bool.to_string.

Mostra una soluzione (prima prova da solo!)
src/duration.gleam
import gleam/bool
import gleam/int
import gleam/io

pub fn main() -> Nil {
  let total = 7384
  let hours = total / 3600
  let minutes = total % 3600 / 60
  let seconds = total % 60

  io.println("Total: " <> int.to_string(total) <> " seconds")
  io.println(
    "Time: "
    <> int.to_string(hours)
    <> " h "
    <> int.to_string(minutes)
    <> " min "
    <> int.to_string(seconds)
    <> " s",
  )
  io.println("More than 2 hours: " <> bool.to_string(total > 2 * 3600))
}

total % 3600 / 60 funziona senza graffe perché % e / hanno la stessa precedenza e vengono calcolati da sinistra a destra: prima il resto, poi la divisione. Per i secondi basta total % 60: le ore e i minuti interi sono tutti multipli di 60, quindi non cambiano il resto.

Ricapitolando

  • + - * / e % (resto) per gli Int; la divisione tra interi taglia i decimali.
  • Dividere per zero dà zero, sia con gli Int sia con i Float: se un divisore può essere zero, controllalo tu.
  • I Float hanno operatori propri: +. -. *. /. e <. >. <=. >=.. Int e Float non si mescolano mai.
  • Conversioni: int.to_float, e verso gli interi float.round o float.truncate.
  • 0.1 +. 0.2 non fa esattamente 0.3: i soldi si contano in centesimi, con gli Int.
  • Niente += né ++: si crea un nuovo let.
  • == e != funzionano su ogni tipo, purché i due lati siano dello stesso tipo.
  • &&, ||, ! per la logica, con il cortocircuito.
  • Per raggruppare si usano le graffe: { 2 + 3 } * 4.

Nella prossima lezione ci concentriamo sull’output: come costruire esattamente il testo che vuoi, con i decimali giusti e le colonne allineate.