Il fallimento è un valore
Non esiste null, e non esistono eccezioni.
Un’operazione può non trovare un valore oppure fallire. In molti linguaggi questi casi usano null, che indica l’assenza di un valore, o le eccezioni, che interrompono l’esecuzione normale. Se il codice non li gestisce, il problema può emergere solo durante l’esecuzione.
Heroes usa un tipo che rappresenta entrambi i risultati: un valore oppure un errore. Non ha null né eccezioni. Dalla dichiarazione di una funzione puoi vedere se può fallire, senza leggerne il corpo.
Un tipo che significa “oppure un errore”
i64? indica un numero intero oppure un errore. Prima di usare il numero devi gestire la possibilità di errore. In una funzione con questo tipo di ritorno, ok(v) restituisce il valore e fail(code, msg) restituisce un errore:
# Parses a decimal digit. Fails on anything else.
function digit_of(c: u8) -> i64?
if c < '0' || c > '9'
return fail(code: "not_a_digit", msg: "expected 0-9")
return ok(to_i64(c - '0').must())
L’errore contiene un codice e un messaggio. Il codice, come "not_a_digit", è un nome stabile in snake_case, cioè parole minuscole separate da trattini bassi. Il programma lo usa per distinguere gli errori e decidere come gestirli. Il messaggio spiega il problema a chi legge e può essere migliorato senza cambiare il codice.
Restituire un errore al chiamante: ?
Per restituire un errore alla funzione chiamante senza gestirlo qui, aggiungi ? alla chiamata. Il simbolo indica il punto in cui la funzione può terminare:
# Reads a whole non-negative number. Empty input is an error, not a zero.
function number_of(text: str) -> i64?
if text.len() == 0
return fail(code: "empty", msg: "there is nothing to read")
total: i64 @= 0
for i in range(from: 0, to: text.len())
total @ total * 10 + digit_of(text[i])?
return ok(total)
digit_of(text[i])? chiama digit_of. Se il risultato è un errore, termina subito la funzione corrente e restituisce lo stesso errore al chiamante. Altrimenti usa il numero restituito e continua il ciclo.
Nell’esempio compaiono anche la modifica di una variabile, gli intervalli e le chiamate con il punto.
total: i64 @= 0 dichiara una variabile modificabile. La riga total @ total * 10 + … le assegna un nuovo valore. Il simbolo = serve invece a definire un nome che non può essere riassegnato. Nomi e modifiche →
range(from: 0, to: text.len()) conta da from fino a
to escluso. I nomi dei due argomenti sono obbligatori
perché tutti e due sono i64.
La regola, e il perché →
text.len() e len(text) sono la stessa identica
chiamata, scritta in due modi. Scrivere con il punto serve solo a leggere da
sinistra a destra quando incateni più operazioni: non c’è nessuna
funzione attaccata al tipo.
Con ? puoi restituire l’errore senza scrivere blocchi try e catch o ripetere il controllo dopo ogni chiamata. Nell’esempio seguente, la funzione calcola la media di una lista e termina al primo carattere non valido:
function average_of(parts: [str]) -> i64?
sum: i64 @= 0
for p in parts
sum @ sum + number_of(p)?
if parts.len() == 0
return fail(code: "empty", msg: "no parts to average")
return ok(sum / parts.len())
Gestire il risultato: match
Usa match per gestire direttamente i due risultati possibili e accedere al loro contenuto:
# Four ways to read a fallible value, in the order you reach for them.
function report(text: str) -> str
match number_of(text)
.ok v => return "read " + v.to_str()
.err e => return "failed: " + e.code + " (" + e.msg + ")"
Ogni ramo indica un caso con un punto iniziale e associa un nome al suo contenuto: .ok v rende disponibile il valore come v. Il simbolo _ scarta un contenuto che non serve. Devi scrivere entrambi i rami: se ne manca uno, il programma non compila. Non è ammesso un ramo catch-all, cioè un ramo per tutti gli altri casi. Questa regola obbliga a rivedere il codice quando cambia l’insieme dei casi possibili.
Quando il fallimento ha già una risposta
Se hai già deciso cosa fare in caso di errore, puoi usare una di queste due forme brevi:
function first_or_zero(parts: [str]) -> i64
return number_of(parts[0]).default(0)
.default(0) restituisce il valore in caso di successo oppure 0 in caso di errore. Il risultato ha tipo i64. .must() restituisce il valore ma termina il programma se trova un errore. Usalo quando hai già verificato che l’operazione riesce, oppure nei test:
test "a good number and a bad one"
assert number_of("120").must() == 120
assert number_of("12x").is_err()
assert report("7") == "read 7"
assert first_or_zero(["oops"]) == 0
Puoi scrivere un test nello stesso file del codice da verificare. Il comando heroes test esegue tutti i test del file.
Tutto insieme, in esecuzione
Il file di esempio mostra tutti e quattro i modi di gestire un valore fallibile: ?, match, .default() e .must(). Questa è la funzione main:
function main()
print(report("42"))
print(report("4b2"))
print(average_of(["10", "20", "30"]).default(-1))
E quello che stampa:
$ heroes run examples/gallery/03-fallible.hero
read 42
failed: not_a_digit (expected 0-9)
20
La seconda riga mostra l’errore prodotto da digit_of sul carattere b di "4b2". number_of lo restituisce a report tramite ?, mantenendo il codice e il messaggio originali.
Che cosa cambia nel codice
Il tipo di ritorno indica se una funzione può fallire. Nelle chiamate, ? segnala dove un errore può terminare la funzione corrente. Per ottenere il valore devi gestire l’errore con match o .default(), oppure usare esplicitamente .must() e accettare che il programma termini in caso di errore.
Poi: Il flusso di controllo:
if, while, for, e perché una
condizione può essere solo un bool.
Torna all’elenco dei capitoli.
← Prima: Variant e match
Esegui un programma tuo: bastano quattro comandi. Oppure prendi tutto il linguaggio in un file solo, e tutti i programmi già scritti.