Heroes / Guida / capitolo 5

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())
examples/gallery/03-fallible.hero, righe 7–11

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)
examples/gallery/03-fallible.hero, righe 13–22

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())
examples/gallery/03-fallible.hero, righe 26–34

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 + ")"
examples/gallery/03-fallible.hero, righe 36–40

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)
examples/gallery/03-fallible.hero, righe 42–43

.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
examples/gallery/03-fallible.hero, righe 45–49

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))
examples/gallery/03-fallible.hero, righe 51–54

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.