Lezione 4 di 6 · 16 min di lettura

Fermarsi di proposito

Non ogni problema merita un Result. todo per il codice non ancora scritto, panic e let assert per le situazioni impossibili, assert per i controlli. E cosa succede quando un programma si ferma.

Due tipi di problemi

Finora abbiamo trattato gli errori come valori: int.parse restituisce Error(Nil), e il programma decide cosa fare. È il modo giusto per i problemi che possono succedere: dati sbagliati, un file che non c’è, un utente che scrive “quaranta” invece di 40.

Ma ci sono anche problemi di un altro genere: situazioni che, se il programma è scritto bene, non dovrebbero mai capitare. Se succedono, non c’è niente di sensato da fare, se non fermarsi e dirlo forte, perché vuol dire che c’è un bug. Per questi casi Gleam ha alcune parole chiave che fermano il programma di proposito. Vanno usate con parsimonia, ma è importante conoscerle.

todo: qui manca ancora qualcosa

Quando scrivi un programma, capita di voler provare una parte prima di aver scritto tutto. todo è un segnaposto: si può mettere al posto di qualsiasi espressione, e il programma compila lo stesso.

src/unfinished.gleam
import gleam/io

pub fn main() -> Nil {
  io.println(describe(3))
}

fn describe(_n: Int) -> String {
  todo as "write describe"
}
output
warning: Todo found
  ┌─ /home/ada/learn-gleam/exercises/src/unfinished.gleam:8:3
  │
8 │   todo as "write describe"
  │   ^^^^^^^^^^^^^^^^^^^^^^^^ This code is incomplete

This code will crash if it is run. Be sure to finish it before
running your program.

Hint: I think its type is `String`.


runtime error: todo

write describe

stacktrace:
  unfinished.main src/unfinished.gleam:8

Due cose succedono. Durante la compilazione, un avviso ti ricorda che il codice è incompleto, e ti dice anche che tipo dovrebbe avere l’espressione mancante (I think its type is String). Durante l’esecuzione, se il programma arriva al todo, si ferma con il messaggio che hai scritto dopo as (che è facoltativo, ma utile quando i todo sono tanti).

todo è uno strumento per lavorare per gradi: scrivi lo scheletro del programma con le firme delle funzioni, metti todo nei corpi, e il compilatore controlla già che i pezzi combacino. Poi li riempi uno alla volta. (Nota il parametro _n: con il trattino basso davanti, il compilatore sa che per ora non lo usi, e non ti avvisa.)

panic: questo non doveva succedere

panic ferma il programma in un punto che, secondo te, non dovrebbe mai essere raggiunto:

src/levels.gleam
import gleam/io

pub fn main() -> Nil {
  io.println(level(-3))
}

fn level(score: Int) -> String {
  case score {
    s if s >= 100 -> "expert"
    s if s >= 0 -> "beginner"
    _ -> panic as "scores are never negative"
  }
}
output
runtime error: panic

scores are never negative

stacktrace:
  levels.main src/levels.gleam:11

La traccia dello stack (stacktrace) dice dove si è fermato il programma: nel file src/levels.gleam, alla riga 11.

La guida ufficiale è severa su panic: non dovrebbe quasi mai servire. Se una funzione può ricevere un punteggio negativo, allora un punteggio negativo può succedere, e il modo giusto di gestirlo è un Result, oppure un tipo che renda impossibile il valore sbagliato, come hai imparato nel modulo 4. panic resta per i prototipi, gli script veloci e i rari casi in cui un’invariante è garantita da qualcos’altro che il compilatore non può vedere.

let assert: un let che può fallire

Ricordi? Un let normale accetta solo pattern che corrispondono sempre: let [first, ..] = lista non compila, perché la lista potrebbe essere vuota. let assert accetta anche pattern parziali: se il valore corrisponde, prosegue; se no, il programma si ferma.

src/first.gleam
pub fn main() -> Nil {
  let assert [first, ..] = [] as "the list should not be empty"
  echo first
  Nil
}
output
runtime error: let assert

the list should not be empty

unmatched value:
  []

stacktrace:
  first.main src/first.gleam:2

Il messaggio mostra anche il valore che non corrispondeva (unmatched value: []), preziosissimo per capire cosa è andato storto. Senza as, il messaggio è un generico Pattern match failed, no pattern matched the value.

let assert è comodo quando sai che un valore ha una certa forma e non vuoi scrivere un case per un caso impossibile. Per esempio, let assert Ok(n) = int.parse("42"): il testo lo hai scritto tu, sai che è un numero. Ma se il testo arriva dall’esterno, let assert è la scelta sbagliata: un dato sbagliato fermerebbe tutto il programma invece di produrre un messaggio d’errore gentile.

assert: controllare che sia vero

L’ultima parola chiave di questa famiglia controlla un Bool: se è True non succede niente, se è False il programma si ferma.

src/checks.gleam
pub fn main() -> Nil {
  assert add(2, 3) == 5
  assert add(2, 2) == 5
}

fn add(a: Int, b: Int) -> Int {
  a + b
}
output
runtime error: assert

Assertion failed.

stacktrace:
  checks.main src/checks.gleam:3

Il primo assert passa, il secondo no. assert è pensato soprattutto per i test, ed è proprio l’argomento della prossima lezione.

Dettagli nerd Cosa vuol dire che un programma «si ferma»? (il codice di uscita)

Quando un programma termina, restituisce al sistema operativo un piccolo numero, il codice di uscita (exit code): 0 significa “tutto bene”, qualsiasi altro numero significa “qualcosa è andato storto”. Nel terminale di Linux e macOS lo vedi subito dopo con echo $?. Un gleam run che finisce normalmente esce con 0; uno che si ferma per un panic, un todo o un assert esce con 1.

Il codice di uscita è il modo in cui i programmi si parlano: uno script che lancia gleam test può sapere se i test sono passati senza leggere quello che è stato stampato, e i servizi che controllano i tuoi progetti (come le GitHub Actions nella cartella .github/ creata da gleam new) usano proprio questo numero per decidere se segnare il controllo in verde o in rosso.

Quiz

Il tuo programma legge un numero digitato da una persona. Quale di queste scelte è la più adatta?

Parola chiaveQuandoCosa succede
todocodice non ancora scrittoavviso in compilazione, stop se eseguito
panicun punto che non deve mai essere raggiuntostop con il messaggio
let assertun pattern che deve corrisponderestop se non corrisponde, con il valore
assertuna condizione che deve essere vera (nei test)stop se è falsa

Tutte accettano as "messaggio". E tutte si usano poco: se un problema può capitare davvero, la risposta è un Result.

Esercizio · sul tuo computer

Prima lo scheletro

Nel progetto exercises crea src/average.gleam in due tempi.

Primo tempo. Scrivi solo lo scheletro, con todo nei corpi:

src/average.gleam
import gleam/int
import gleam/io

pub fn main() -> Nil {
  let grades = [28, 24, 23]
  io.println("Grades: " <> join(grades))
  io.println("Average: " <> int.to_string(average(grades)))
}

fn join(_numbers: List(Int)) -> String {
  todo as "join the numbers with commas"
}

fn average(_grades: List(Int)) -> Int {
  todo as "compute the average"
}

Lancia gleam run -m average e osserva gli avvisi e il punto in cui il programma si ferma. (I parametri hanno il trattino basso davanti perché per ora non li usi; toglilo quando scrivi i corpi.)

Secondo tempo. Sostituisci i todo. In average, controlla con un let assert che la lista non sia vuota (let assert [_, ..] = grades as "..."), poi dividi la somma per la lunghezza. L’output finale:

output
Grades: 28, 24, 23
Average: 25
Mostra una soluzione (prima prova da solo!)
src/average.gleam
import gleam/int
import gleam/io
import gleam/list
import gleam/string

pub fn main() -> Nil {
  let grades = [28, 24, 23]
  io.println("Grades: " <> join(grades))
  io.println("Average: " <> int.to_string(average(grades)))
}

fn join(numbers: List(Int)) -> String {
  numbers |> list.map(int.to_string) |> string.join(with: ", ")
}

fn average(grades: List(Int)) -> Int {
  let assert [_, ..] = grades as "average of an empty list"
  int.sum(grades) / list.length(grades)
}

Il let assert documenta un’aspettativa: chi chiama average deve passare almeno un voto. Qui va bene, perché la lista è scritta nel programma. Se i voti arrivassero dall’esterno, sarebbe meglio che average restituisse un Result (o un Option), come list.first. (E ricorda: senza quel controllo, con una lista vuota la divisione per zero darebbe 0, un risultato sbagliato ma silenzioso.)

Ricapitolando

  • I problemi che possono succedere si gestiscono con Result; quelli che non dovrebbero mai succedere possono fermare il programma.
  • todo è un segnaposto: avviso in compilazione (con il tipo atteso), stop se eseguito.
  • panic ferma il programma in un punto impossibile; da usare pochissimo.
  • let assert pattern = valore accetta pattern parziali, e si ferma mostrando il valore se non corrispondono.
  • assert condizione si ferma se la condizione è falsa: serve soprattutto nei test.
  • Tutte accettano as "messaggio". Un programma fermato così esce con codice 1.

Nella prossima lezione usiamo assert per quello per cui è nato: scrivere test, e lanciarli con gleam test.