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
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 => "`)`"
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
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))
$ 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.