Lezione 4 di 8 · 20 min di lettura
Decidere con case
In Gleam non c'è if. C'è qualcosa di meglio, case, e un compilatore che si accorge se hai dimenticato un caso.
Niente if
Un programma che fa sempre la stessa cosa serve a poco: prima o poi deve scegliere. Se il voto è sufficiente, “promosso”, altrimenti “bocciato”. Se la password è giusta, entra. In quasi tutti i linguaggi per questo c’è if. In Gleam no: non esiste if, non esiste switch, non esiste else. Esiste una sola costruzione per decidere, e si chiama case.
Sembra una limitazione. Alla fine della lezione vedrai che è il contrario.
La prima scelta
import gleam/io
pub fn main() -> Nil {
io.println(medal(1))
io.println(medal(3))
io.println(medal(7))
}
fn medal(position: Int) -> String {
case position {
1 -> "Gold"
2 -> "Silver"
3 -> "Bronze"
_ -> "No medal"
}
}Gold
Bronze
No medalLeggiamola:
case position { ... }dice: “guarda il valore diposition”. Quel valore si chiama il soggetto delcase.- Dentro ci sono le clausole, una per riga, nella forma
pattern -> risultato. Il pattern (modello) descrive una forma che il soggetto potrebbe avere; il risultato è cosa fare se ce l’ha. - Le clausole vengono provate dall’alto in basso, e vince la prima che corrisponde. Le altre vengono ignorate.
- Il trattino basso
_è il pattern che corrisponde a qualsiasi valore: è il “in tutti gli altri casi”.
Con medal(7) il 7 non è né 1, né 2, né 3, e finisce nel _.
Questo modo di decidere, confrontando un valore con una serie di forme, si chiama pattern matching. Per ora le forme sono semplici numeri; nelle prossime lezioni diventeranno molto più interessanti.
case è un’espressione
case non “esegue un ramo”: vale qualcosa, il risultato della clausola scelta. Per questo medal può restituirlo direttamente, e per questo lo puoi mettere in un let:
let label = case position {
1 -> "Gold"
_ -> "Other"
}La conseguenza è che tutte le clausole devono produrre un valore dello stesso tipo. Se una restituisce una String e un’altra un Int, che tipo avrebbe label? Il compilatore non te lo lascia fare:
import gleam/io
pub fn main() -> Nil {
io.println(medal(1))
}
fn medal(position: Int) -> String {
case position {
1 -> "Gold"
_ -> 0
}
}error: Type mismatch
┌─ /home/ada/learn-gleam/exercises/src/medals.gleam:10:5
│
10 │ _ -> 0
│ ^^^^^^
This case clause was found to return a different type than the previous
one, but all case clauses must return the same type.
Expected type:
String
Found type:
IntIl compilatore conta i casi per te
Ecco la vera forza di case. Prova a togliere la clausola _ da medal:
import gleam/io
pub fn main() -> Nil {
io.println(medal(1))
}
fn medal(position: Int) -> String {
case position {
1 -> "Gold"
2 -> "Silver"
3 -> "Bronze"
}
}error: Inexhaustive patterns
┌─ /home/ada/learn-gleam/exercises/src/medals.gleam:8:3
│
8 │ ╭ case position {
9 │ │ 1 -> "Gold"
10 │ │ 2 -> "Silver"
11 │ │ 3 -> "Bronze"
12 │ │ }
│ ╰───^
This case expression does not have a pattern for all possible values. If it
is run on one of the values without a pattern then it will crash.
The missing patterns are:
_Il programma non compila. Il compilatore ha capito che position è un Int, che gli Int sono molti più di tre, e che per 4 (o 7, o -1) il case non saprebbe cosa fare. Ti elenca anche cosa manca: _, cioè “tutti gli altri numeri”. Questo controllo si chiama verifica di esaustività, ed è uno dei motivi per cui i programmi Gleam sono così affidabili: un caso dimenticato non diventa un bug scoperto dopo mesi, diventa un errore di compilazione.
Il controllo funziona anche al contrario. Se scrivi una clausola che non potrà mai essere raggiunta, perché una precedente cattura già tutti i suoi valori, ricevi un avviso:
import gleam/io
pub fn main() -> Nil {
io.println(medal(1))
}
fn medal(position: Int) -> String {
case position {
_ -> "No medal"
1 -> "Gold"
}
}warning: Unreachable pattern
┌─ /home/ada/learn-gleam/exercises/src/medals.gleam:10:5
│
10 │ 1 -> "Gold"
│ ^
This pattern cannot be reached as a previous pattern matches the same
values.
Hint: It can be safely removed.
No medalIl _ in cima vince sempre, quindi l’oro non arriva mai. La regola pratica: i casi particolari in alto, quelli generali in basso.
Quiz
Cosa restituisce medal(2) se le clausole sono, in ordine: 2 -> "Silver", _ -> "No medal", 2 -> "Second"?
Dare un nome al valore
Al posto di _ puoi scrivere un nome. Anche lui corrisponde a qualsiasi valore, ma in più lo lega a quel nome, che puoi usare nel risultato della clausola:
import gleam/int
import gleam/io
pub fn main() -> Nil {
io.println(position_text(1))
io.println(position_text(12))
}
fn position_text(position: Int) -> String {
case position {
1 -> "Winner!"
other -> "Arrived " <> int.to_string(other) <> "th"
}
}Winner!
Arrived 12thother esiste solo dentro la sua clausola. (Qui avremmo potuto usare direttamente position, che ha lo stesso valore; il nome nel pattern diventerà indispensabile quando i pattern avranno più pezzi, tra poche lezioni.)
Il posto di if: case su un Bool
E per le decisioni sì/no? Un confronto come age >= 18 produce un Bool, e un Bool ha esattamente due valori: True e False. Basta farne il soggetto di un case:
import gleam/io
pub fn main() -> Nil {
io.println(describe(34))
io.println(describe(12))
}
fn describe(age: Int) -> String {
case age >= 18 {
True -> "adult"
False -> "minor"
}
}adult
minorQuesto è l’if di Gleam. Non è un trucco da principianti: è così che è scritta anche la libreria standard. Per esempio int.absolute_value, il valore assoluto, è:
pub fn absolute_value(x: Int) -> Int {
case x >= 0 {
True -> x
False -> x * -1
}
}E la verifica di esaustività vale anche qui: se scrivi solo la clausola True, il compilatore ti ricorda che manca False (The missing patterns are: False). Non esiste un if senza else che “non fa niente”: ogni case deve dire cosa vale in ogni situazione.
Più righe in una clausola
Il risultato di una clausola è una espressione. Se ti servono più passaggi, usa un blocco: le graffe raggruppano più espressioni, e il blocco vale l’ultima.
case age >= 18 {
True -> {
let years = age - 18
"adult for " <> int.to_string(years) <> " years"
}
False -> "minor"
}Se però una clausola diventa lunga, di solito è il segnale che quel pezzo merita una funzione tutta sua.
Più valori, stessa risposta
Spesso più valori portano allo stesso risultato. Invece di ripetere la clausola, puoi elencare dei pattern alternativi separati da |:
import gleam/io
pub fn main() -> Nil {
io.println(kind(6))
io.println(kind(3))
io.println(kind(9))
}
fn kind(day: Int) -> String {
case day {
6 | 7 -> "weekend"
1 | 2 | 3 | 4 | 5 -> "workday"
_ -> "not a day"
}
}weekend
workday
not a daySi legge “6 oppure 7”. La clausola vince se il soggetto corrisponde ad almeno uno dei pattern.
I pattern possono anche essere stringhe, che corrispondono solo se sono esattamente uguali (maiuscole comprese):
case answer {
"yes" | "y" -> True
_ -> False
}Quiz
Cosa restituisce case 0 { 0 | 1 -> "small" n -> "big" }?
Esercizio · sul tuo computer
I giorni della settimana
Nel progetto exercises crea src/weekdays.gleam con due funzioni:
day_name(day: Int) -> String: da 1 a 7 restituisce il nome del giorno in inglese (1 è"Monday", 7 è"Sunday"), per qualsiasi altro numero"Invalid day";day_kind(day: Int) -> String:"weekend"per 6 e 7,"workday"da 1 a 5,"invalid"altrimenti. Usa i pattern alternativi.
Poi aggiungi una funzione describe(day: Int) -> String che mette insieme le due, e in main stampa describe di 1, 6, 7 e 0. L’output deve essere:
Monday: workday
Saturday: weekend
Sunday: weekend
Invalid day: invalidMostra una soluzione (prima prova da solo!)
import gleam/io
pub fn main() -> Nil {
io.println(describe(1))
io.println(describe(6))
io.println(describe(7))
io.println(describe(0))
}
fn describe(day: Int) -> String {
day_name(day) <> ": " <> day_kind(day)
}
fn day_name(day: Int) -> String {
case day {
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
7 -> "Sunday"
_ -> "Invalid day"
}
}
fn day_kind(day: Int) -> String {
case day {
6 | 7 -> "weekend"
1 | 2 | 3 | 4 | 5 -> "workday"
_ -> "invalid"
}
}Prova a togliere la clausola _ da una delle due funzioni: il compilatore ti mostrerà l’errore Inexhaustive patterns.
Ricapitolando
- In Gleam non ci sono
if,elsenéswitch: si decide concase soggetto { pattern -> risultato }. - Le clausole si provano dall’alto in basso, e vince la prima che corrisponde.
_corrisponde a tutto; un nome corrisponde a tutto e lega il valore a quel nome.caseè un’espressione: vale il risultato della clausola scelta, e tutte le clausole devono avere lo stesso tipo.- Il compilatore verifica l’esaustività (
Inexhaustive patterns) e segnala le clausole irraggiungibili (Unreachable pattern). - Per le decisioni sì/no si fa
casesu unBool:True -> ...,False -> .... - Più passaggi in una clausola: un blocco
{ ... }. Più valori con la stessa risposta:6 | 7 -> ....
Nella prossima lezione i pattern diventano più furbi: pezzi di stringa, più soggetti insieme e condizioni aggiuntive con le guardie.