Heroes / Guida / capitolo 4

Variant e match

Definire i casi di un valore e gestirli con match.

Un record contiene tutti i suoi campi: un punto ha una x e una y. Un variant rappresenta invece uno solo dei casi dichiarati. Per esempio, un token, cioè un elemento del testo analizzato dal compilatore, può essere un numero, una parola o un segno più. Alcuni casi contengono dati aggiuntivi.

Come si dichiara

variant Token
    number
        value: i64
    word
        text: str
    plus
    times
    open
    close
examples/gallery/02-tokens.hero, righe 7–15

Il tipo ha sei casi. number e word contengono dati dichiarati come campi indentati sotto il nome del caso. Gli altri quattro non hanno campi: plus è già un valore completo.

Come si legge: match

match riconosce il caso di un variant e permette di accedere ai suoi campi nello stesso ramo:

# What a token is called in a message.
function name_of(t: Token) -> str
    return match t
        .number _ => "a number"
        .word _   => "a word"
        .plus     => "`+`"
        .times    => "`*`"
        .open     => "`(`"
        .close    => "`)`"
examples/gallery/02-tokens.hero, righe 17–25

Ogni ramo indica il caso con un punto iniziale, seguito da => e dal risultato. Se il caso contiene dati, puoi associarli a un nome: in .number n, n.value è il numero contenuto. Qui quei dati non servono, quindi si usa _, che non dichiara un nome e può essere ripetuto.

match è un’espressione: ha un valore, ed è per questo che tutta la funzione è un return solo. Può anche stare da sola come istruzione, quando i rami fanno qualcosa invece di produrre qualcosa.

Tutti i casi, o non compila

Lasciane fuori uno e la compilazione si ferma. Questo è il file di sopra con l’ultimo ramo cancellato:

$ heroes check tokens.hero
error[non_exhaustive]: this `match` does not cover `.close`
  at tokens.hero:19:12
     |
  19 |     return match t
     |            ^

Il ramo _ è vietato sui variant

Su un variant, Heroes vieta il ramo _ che gestirebbe tutti i casi non elencati. Il messaggio spiega il motivo:

$ heroes check tokens.hero
error[wildcard_on_variant]: `_` is not allowed as an arm over `Token` — name every case, so that adding one breaks this `match` instead of silently falling through
  at tokens.hero:25:9
     |
  25 |         _         => "something else"
     |         ^

Puoi usare _ in un match su un i64 o uno str, per gestire tutti i valori non elencati. Per questi tipi non sarebbe pratico scrivere un ramo per ogni valore.

Aggiungere un caso richiede di aggiornare i match

Aggiungi a Token un settimo caso, minus, e ripeti il controllo:

$ heroes check tokens.hero
error[non_exhaustive]: this `match` does not cover `.minus`
  at tokens.hero:20:12
     |
  20 |     return match t
     |            ^

error[non_exhaustive]: this `match` does not cover `.minus`
  at tokens.hero:30:12
     |
  30 |     return match t
     |            ^

error[non_exhaustive]: this `match` does not cover `.minus`
  at tokens.hero:38:12
     |
  38 |     return match t
     |            ^

Il compilatore segnala tre errori, uno per ogni match che non gestisce il nuovo caso. Devi decidere il nome del segno meno, la sua precedenza e se considerarlo un operatore. La compilazione può proseguire solo quando tutti i casi sono gestiti.

Due casi, una risposta sola

Se più casi producono lo stesso risultato, | permette di elencarli nello stesso ramo:

function is_operator(t: Token) -> bool
    return match t
        .plus | .times                       => true
        .number _ | .word _ | .open | .close => false
examples/gallery/02-tokens.hero, righe 36–39

Ogni caso resta esplicito. Aggiungere minus richiede quindi di aggiornare anche questa funzione, decidendo se deve restituire vero o falso per il nuovo caso.

Come creare un valore

function main()
    # The annotation is required and it is not ceremony: a case says which
    # case it is, never which variant, so a literal list of them has nothing
    # to infer from. Name the type once and the whole list is checked.
    line: [Token] = [.open, .number(value: 12), .plus, .word(text: "x"), .close]

    for t in line
        print(name_of(t))
examples/gallery/02-tokens.hero, righe 46–53
$ heroes run examples/gallery/02-tokens.hero
`(`
a number
`+`
a word
`)`

Un caso senza campi si scrive .plus. Per un caso con campi si usa .number(value: 12), con i nomi come nei record. Il punto iniziale indica il caso, ma il compilatore deve conoscere anche il tipo. L’annotazione : [Token] lo specifica per tutti gli elementi della lista.

Poi: Il fallimento è un valore, che è l’idea di questo capitolo applicata all’unico caso che ha ogni programma: ha funzionato, oppure no. Torna all’elenco dei capitoli. ← Prima: Record e funzioni

Esegui un programma tuo: bastano quattro comandi. Oppure prendi tutto il linguaggio in un file solo, e tutti i programmi già scritti.