Lezione 2 di 6 · 18 min di lettura
Tipi con varianti
Il primo tipo tutto tuo. Un tipo che può avere solo i valori che decidi tu, e un compilatore che ti avvisa ogni volta che ne dimentichi uno.
Le stagioni, come stringhe
Supponiamo di scrivere un programma sul tempo atmosferico, con le stagioni. Con quello che sappiamo finora, la stagione sarebbe una stringa:
fn weather(season: String) -> String {
case season {
"spring" -> "Mild"
"sumer" -> "Hot"
"autumn" -> "Windy"
"winter" -> "Cold"
_ -> "Unknown season"
}
}Hai visto l’errore? "sumer" al posto di "summer". Il compilatore non se ne accorge: per lui è una stringa come un’altra. Con weather("summer") il programma risponde "Unknown season", in piena estate, e nessuno ti avvisa. E quella clausola _ finale, obbligatoria perché le stringhe possibili sono infinite, nasconde il problema invece di segnalarlo.
Il punto è che le stagioni non sono “una stringa qualsiasi”: sono quattro, e basta. Ci serve un tipo che possa avere solo quattro valori.
Il tipo Season
import gleam/io
pub type Season {
Spring
Summer
Autumn
Winter
}
pub fn main() -> Nil {
io.println(weather(Spring))
io.println(weather(Autumn))
}
fn weather(season: Season) -> String {
case season {
Spring -> "Mild"
Summer -> "Hot"
Autumn -> "Windy"
Winter -> "Cold"
}
}Mild
Windytype Season { ... } definisce un tipo personalizzato (custom type) di nome Season. Dentro le graffe ci sono le sue varianti, i soli valori possibili: Spring, Summer, Autumn, Winter. Ogni variante è anche un valore che puoi scrivere nel codice, come True o 42: weather(Spring).
Le regole dei nomi sono quelle che il compilatore ti ricordava nel modulo 1: il nome del tipo e quelli delle varianti in PascalCase, con la maiuscola. Se scrivi type season, ricevi un I'm expecting a type name here, con il suggerimento che i nomi dei tipi cominciano con la maiuscola.
Ora un errore di battitura come Sumer non passa più: non esiste nessuna variante con quel nome, e il programma non compila. E non serve più la clausola _: le quattro clausole coprono tutti i valori possibili, e il compilatore lo sa.
Il compilatore come assistente
Il vantaggio più grande si vede quando il programma cresce. Supponiamo di dimenticare una stagione:
import gleam/io
pub type Season {
Spring
Summer
Autumn
Winter
}
pub fn main() -> Nil {
io.println(weather(Spring))
}
fn weather(season: Season) -> String {
case season {
Spring -> "Mild"
Summer -> "Hot"
Autumn -> "Windy"
}
}error: Inexhaustive patterns
┌─ /home/ada/learn-gleam/exercises/src/seasons.gleam:15:3
│
15 │ ╭ case season {
16 │ │ Spring -> "Mild"
17 │ │ Summer -> "Hot"
18 │ │ Autumn -> "Windy"
19 │ │ }
│ ╰───^
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:
WinterIl compilatore non si limita a dire che manca qualcosa: dice cosa. Ora immagina di lavorare a un programma vero, con cinquanta case sulle stagioni sparsi in venti file, e di dover aggiungere una quinta stagione (in qualche paese c’è la stagione dei monsoni). Aggiungi la variante al tipo, lanci gleam build, e il compilatore ti porta uno per uno in tutti i punti che vanno aggiornati. Nessuno sfugge. È il motivo per cui in Gleam si modifica il codice con una tranquillità che in altri linguaggi non c’è.
Funzioni che restituiscono varianti
Un tipo personalizzato si usa come qualsiasi altro tipo: nei parametri, nei risultati, nelle liste. Una funzione che dà la stagione successiva:
fn next(season: Season) -> Season {
case season {
Spring -> Summer
Summer -> Autumn
Autumn -> Winter
Winter -> Spring
}
}E le varianti si confrontano con ==: next(Winter) == Spring è True. Con echo vedi il loro nome, Spring, e una lista di stagioni ha tipo List(Season).
Quiz
Con il tipo Season di questa lezione, cosa succede con weather(Spring) se nel case scrivi le clausole in quest'ordine: Winter, Autumn, Summer, Spring?
Bool è un tipo come questo
Una sorpresa: Bool non è niente di speciale. È un tipo con due varianti, True e False, ed è per questo che si scrivono con la maiuscola, e che un case su un Bool deve gestirle entrambe. Se Gleam non lo avesse già, potresti definirlo tu:
pub type Bool {
True
False
}Anche Nil segue lo stesso schema: è un tipo con una sola variante, Nil.
Dettagli nerd Cos'è un atomo? (come diventa una variante sulla BEAM)
Quando Gleam compila per la BEAM, traduce il tuo codice in Erlang. E in Erlang esiste un tipo di valore fatto apposta per le varianti: l’atomo, una costante che è semplicemente un nome. Spring diventa l’atomo spring, e anche True, False e Nil diventano gli atomi true, false e nil.
La BEAM tiene una tabella di tutti gli atomi del programma, e ogni atomo in memoria è solo un piccolo numero che punta a quella tabella. Confrontare due atomi, quindi, è velocissimo: si confrontano due numeri, non due testi. È uno dei motivi per cui un case su un tipo personalizzato non costa praticamente nulla.
pub type
Hai notato il pub davanti a type Season? Come per le funzioni, un tipo senza pub è privato del suo modulo. E c’è una regola in più: una funzione pubblica non può usare un tipo privato nella sua firma. Se Season fosse privato e weather fosse pub, il compilatore risponderebbe Private type used in public interface: chi chiama weather da un altro modulo non saprebbe cosa passarle. Per i tipi di un esercizio, pub type va benissimo.
E se una variante di un tipo privato non viene mai usata, ricevi un avviso Unused private constructor, come per le funzioni private.
Esercizio · sul tuo computer
Il semaforo
Nel progetto exercises crea src/traffic.gleam con:
- un tipo
Lightcon tre varianti:Red,Yellow,Green; next(light: Light) -> Light: dopo il rosso viene il verde, dopo il verde il giallo, dopo il giallo il rosso;name(light: Light) -> String: il nome in minuscolo;can_cross(light: Light) -> Bool: si attraversa solo con il verde. Qui non scrivere tre clausole: basta un confronto.
In main, con la lista [Red, Green, Yellow], stampa una riga per ogni luce con la successiva, e poi una riga con can_cross di ognuna (usa list.map e string.join):
red -> green
green -> yellow
yellow -> red
Can cross: False, True, FalseMostra una soluzione (prima prova da solo!)
import gleam/bool
import gleam/io
import gleam/list
import gleam/string
pub type Light {
Red
Yellow
Green
}
pub fn main() -> Nil {
let lights = [Red, Green, Yellow]
lights
|> list.each(fn(light) {
io.println(name(light) <> " -> " <> name(next(light)))
})
let answers =
lights
|> list.map(fn(light) { bool.to_string(can_cross(light)) })
|> string.join(with: ", ")
io.println("Can cross: " <> answers)
}
fn next(light: Light) -> Light {
case light {
Red -> Green
Green -> Yellow
Yellow -> Red
}
}
fn name(light: Light) -> String {
case light {
Red -> "red"
Yellow -> "yellow"
Green -> "green"
}
}
fn can_cross(light: Light) -> Bool {
light == Green
}can_cross con un == è più corta di un case. Ma c’è un prezzo: se un giorno aggiungessi una variante FlashingGreen, il compilatore non ti chiederebbe se si può attraversare, mentre con un case completo sì. Scegli il case quando vuoi che ogni nuova variante ti obblighi a pensarci.
Ricapitolando
pub type Season { Spring Summer Autumn Winter }definisce un tipo con quattro varianti, i soli valori possibili.- Nomi dei tipi e delle varianti in
PascalCase. - Un
casesu un tipo personalizzato non ha bisogno di_, e il compilatore dice quali varianti mancano. - Aggiungendo una variante, il compilatore ti porta in tutti i punti da aggiornare.
- Le varianti si passano, si restituiscono, si confrontano con
==e si mettono nelle liste. BooleNilsono tipi fatti allo stesso modo.- Una funzione
pubnon può usare un tipo privato nella firma.
Finora le varianti erano solo nomi. Nella prossima lezione impariamo a metterci dentro dei dati.