Lezione 2 di 6 · 16 min di lettura
Pattern sulle liste
Smontare una lista con case. La lista vuota, le liste di lunghezza precisa, e il pattern più importante di tutti, [first, ..rest].
Una lista ha una forma
Nel modulo precedente i pattern di case confrontavano numeri e stringhe. Ma i pattern danno il meglio con le liste, perché una lista ha una forma: può essere vuota, avere un elemento, due, tanti. E ogni forma si può descrivere con un pattern che assomiglia alla lista stessa.
import gleam/int
import gleam/io
import gleam/list
pub fn main() -> Nil {
io.println(describe([]))
io.println(describe([7]))
io.println(describe([7, 8]))
io.println(describe([7, 8, 9, 10]))
}
fn describe(items: List(Int)) -> String {
case items {
[] -> "empty"
[x] -> "just " <> int.to_string(x)
[x, y] -> "a pair: " <> int.to_string(x) <> " and " <> int.to_string(y)
[first, ..rest] ->
"starts with "
<> int.to_string(first)
<> ", then "
<> int.to_string(list.length(rest))
<> " more"
}
}empty
just 7
a pair: 7 and 8
starts with 7, then 3 moreClausola per clausola:
[]corrisponde solo alla lista vuota.[x]corrisponde a una lista con esattamente un elemento, e lo chiamax.[x, y]corrisponde a una lista con esattamente due elementi.[first, ..rest]corrisponde a una lista con almeno un elemento: il primo si chiamafirst, e tutti gli altri, come lista, si chiamanorest.
Nota che [7, 8] corrisponderebbe anche all’ultima clausola, ma vince [x, y] che sta sopra: vale sempre la regola “dall’alto in basso, vince la prima”.
Il primo, e il resto
[first, ..rest] è il pattern più importante di questo modulo, e merita un’occhiata da vicino. È la stessa sintassi che usi per costruire una lista con un elemento in testa ([x, ..names]), usata al contrario, per smontarla.
| Lista | first | rest |
|---|---|---|
[7, 8, 9] | 7 | [8, 9] |
[7, 8] | 7 | [8] |
[7] | 7 | [] |
[] | non corrisponde |
Ricordi le celle della lezione precedente? first è l’elemento della prima cella, rest è la catena a cui la prima cella punta. Separarli non costa niente: nessuna copia, solo due nomi. Ed è per questo che in Gleam le liste si leggono dall’inizio: il primo elemento è sempre a portata di mano.
I nomi first e rest sono una convenzione diffusa, ma puoi usare quelli che vuoi: [head, ..tail], [song, ..other_songs]. Se il resto non ti interessa, scrivi solo i due puntini: [first, ..].
Quiz
Con la lista ["a"], a cosa corrisponde il pattern [first, ..rest]?
Pattern più precisi
Dentro un pattern di lista puoi mettere altri pattern: valori esatti, trattini bassi, nomi. E si combinano con le guardie.
import gleam/io
pub fn main() -> Nil {
io.println(start([1, 5]))
io.println(start([200, 5]))
io.println(start([3, 4, 5]))
io.println(start([3]))
}
fn start(items: List(Int)) -> String {
case items {
[1, ..] -> "starts with one"
[first, ..] if first > 100 -> "starts big"
[_, _, ..] -> "at least two"
_ -> "something else"
}
}starts with one
starts big
at least two
something else[1, ..]: comincia con 1, poi qualsiasi cosa (anche niente).[first, ..] if first > 100: almeno un elemento, e il primo è maggiore di 100.[_, _, ..]: almeno due elementi, di cui non ci interessa il valore.
Il compilatore conta anche le forme
La verifica di esaustività funziona anche con le liste, ed è sorprendentemente precisa. Se gestisci solo la lista vuota e quella con un elemento, il compilatore ti dice esattamente quale forma manca:
import gleam/io
pub fn main() -> Nil {
io.println(count_word([1]))
}
fn count_word(items: List(Int)) -> String {
case items {
[] -> "none"
[_] -> "one"
}
}error: Inexhaustive patterns
┌─ /home/ada/learn-gleam/exercises/src/shapes.gleam:8:3
│
8 │ ╭ case items {
9 │ │ [] -> "none"
10 │ │ [_] -> "one"
11 │ │ }
│ ╰───^
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:
[_, _, ..]“Mancano le liste con almeno due elementi.” Aggiungi _ -> "many" (o [_, _, ..] -> "many") e compila.
E il pattern per l’ultimo elemento? Non esiste. Se provi [..rest, last], il compilatore risponde che per trovare la fine bisognerebbe percorrere tutta la lista, e che quindi non c’è una sintassi apposta: Pattern match on the start of the list instead. Anche qui, le liste si leggono dall’inizio.
Il primo elemento, o un ripiego
Un uso tipico: prendere il primo elemento di una lista, con un valore di riserva se la lista è vuota.
fn first_or(items: List(Int), default: Int) -> Int {
case items {
[] -> default
[first, ..] -> first
}
}first_or([4, 5], 0) vale 4, first_or([], 0) vale 0. (La libreria standard ha list.first, ma restituisce un tipo che ancora non conosciamo, Result, pensato proprio per dire “potrebbe non esserci”. Arriverà in un modulo tutto suo.)
Dare un nome a un pezzo intero
A volte ti serve sia controllare la forma di un pezzo sia averlo tutto intero. La parola as dà un nome a un sotto-pattern:
case lists {
[[_, ..] as found, ..] -> found
_ -> []
}Si legge: “una lista il cui primo elemento è una lista con almeno un elemento; chiama found quel primo elemento”. Con [[], [1, 2]] non corrisponde (la prima lista è vuota); con [[1, 2], []] sì, e found vale [1, 2].
Esercizio · sul tuo computer
Chi viene alla festa?
Nel progetto exercises crea src/party.gleam con una funzione guests(names: List(String)) -> String che descrive una lista di invitati:
- nessuno:
"Nobody"; - uno: il suo nome;
- due:
"Ada and Grace"; - tre:
"Ada, Grace and Joe"; - quattro o più: i primi due, e quanti altri, come
"Ada, Grace and 2 others".
In main stampa guests di: una lista vuota, ["Ada"], ["Ada", "Grace"], ["Ada", "Grace", "Joe"] e ["Ada", "Grace", "Joe", "Linus"]. L’output deve essere:
Nobody
Ada
Ada and Grace
Ada, Grace and Joe
Ada, Grace and 2 othersSuggerimento: una clausola per ogni forma, e per l’ultima [a, b, ..rest] con list.length(rest).
Mostra una soluzione (prima prova da solo!)
import gleam/int
import gleam/io
import gleam/list
pub fn main() -> Nil {
io.println(guests([]))
io.println(guests(["Ada"]))
io.println(guests(["Ada", "Grace"]))
io.println(guests(["Ada", "Grace", "Joe"]))
io.println(guests(["Ada", "Grace", "Joe", "Linus"]))
}
fn guests(names: List(String)) -> String {
case names {
[] -> "Nobody"
[a] -> a
[a, b] -> a <> " and " <> b
[a, b, c] -> a <> ", " <> b <> " and " <> c
[a, b, ..rest] ->
a <> ", " <> b <> " and " <> int.to_string(list.length(rest)) <> " others"
}
}L’ordine conta: con tre invitati corrisponderebbe anche [a, b, ..rest] (con rest uguale a ["Joe"]), ma [a, b, c] sta sopra e vince. Così non esce mai il brutto "and 1 others".
Ricapitolando
[]corrisponde alla lista vuota;[x],[x, y]a liste di lunghezza esatta.[first, ..rest]corrisponde a una lista con almeno un elemento: il primo e il resto. Separarli non costa niente.[first, ..]se il resto non serve;[_, _, ..]per “almeno due”.- Dentro i pattern di lista vanno valori esatti,
_, nomi, e fuori le guardie. - Il compilatore controlla le forme mancanti (
[_, _, ..]); non esiste un pattern per l’ultimo elemento. pattern as nomedà un nome a un sotto-pattern intero.
Nella prossima lezione mettiamo insieme [first, ..rest] e la ricorsione del modulo 2, per percorrere una lista dall’inizio alla fine.