Lezione 1 di 6 · 18 min di lettura
Le liste
Tanti valori dello stesso tipo, in fila. Come si scrive una lista, perché si aggiunge in testa e mai in coda, e perché non esiste lista[0].
Tanti valori, un nome solo
Finora ogni variabile conteneva un valore solo: un voto, un nome, un prezzo. Ma i programmi veri lavorano quasi sempre con tanti valori dello stesso genere: i voti di una classe, i prodotti di un carrello, le righe di un file. Per questo esistono le liste.
import gleam/io
pub fn main() -> Nil {
let scores = [18, 25, 30]
let names = ["Ada", "Grace", "Joe"]
let nothing = []
echo scores
echo names
echo nothing
io.println("Done")
}src/first_lists.gleam:7
[18, 25, 30]
src/first_lists.gleam:8
["Ada", "Grace", "Joe"]
src/first_lists.gleam:9
[]
DoneUna lista si scrive tra parentesi quadre, con gli elementi separati da virgole. [] è la lista vuota. Come per le chiamate di funzione, se una lista va a capo il formattatore mette ogni elemento su una riga e una virgola anche dopo l’ultimo.
Il tipo di una lista
Il tipo di scores è List(Int): “una lista di Int”. Quello di names è List(String). Tra le parentesi c’è il tipo degli elementi, e le annotazioni si scrivono così:
let scores: List(Int) = [18, 25, 30]La regola è rigida: tutti gli elementi di una lista hanno lo stesso tipo. Una lista che mescola numeri e testo non compila:
import gleam/io
pub fn main() -> Nil {
let items = [1, "two", 3]
echo items
io.println("Done")
}error: Type mismatch
┌─ /home/ada/learn-gleam/exercises/src/first_lists.gleam:4:19
│
4 │ let items = [1, "two", 3]
│ ^^^^^
All elements of a list must be the same type, but this one doesn't
match the one before it.
Expected type:
Int
Found type:
StringPuò sembrare una limitazione, ma è quello che rende le liste comode: se hai una List(Int), sai che ogni elemento è un numero, e puoi sommarli senza controllare niente. (Per tenere insieme valori di tipo diverso esistono altre strutture, che vedremo nel prossimo modulo.)
List ricorda le funzioni generiche della lezione 2.3: è un tipo con una variabile di tipo dentro. Per questo la lista vuota [] va bene per qualsiasi tipo di elemento: il compilatore decide quale guardando come la usi.
Stampare una lista
io.println vuole una String, e le liste non sono stringhe. Per trasformare una lista (o qualsiasi valore) nel testo che la rappresenta c’è string.inspect:
io.println(string.inspect([18, 25, 30]))stampa [18, 25, 30], esattamente come echo ma senza la riga con il file, e sullo standard output. È comoda per esercizi e prove; per un output “vero” di solito si costruisce il testo in modo più curato, come vedremo nelle prossime lezioni.
Aggiungere in testa
Le liste, come tutto in Gleam, non si modificano. Ma puoi creare una nuova lista che ha un elemento in più all’inizio:
import gleam/io
import gleam/string
pub fn main() -> Nil {
let names = ["Grace", "Joe"]
let more = ["Ada", ..names]
io.println(string.inspect(more))
io.println(string.inspect(names))
}["Ada", "Grace", "Joe"]
["Grace", "Joe"]["Ada", ..names] si legge ”"Ada", seguita da tutti gli elementi di names”. I due puntini (lo spread) dicono “il resto della lista è questa”. E names non è cambiata: è ancora ["Grace", "Joe"].
Puoi anche aggiungerne più di uno: [1, 2, ..rest]. Ma i due puntini devono stare in fondo. Se provi ad aggiungere in coda, con [..names, "Ada"], il compilatore ti ferma e ti spiega perché:
import gleam/io
import gleam/string
pub fn main() -> Nil {
let names = ["Grace", "Joe"]
let all = [..names, "Ada"]
io.println(string.inspect(all))
}error: Syntax error
┌─ /home/ada/learn-gleam/exercises/src/prepend.gleam:6:14
│
6 │ let all = [..names, "Ada"]
│ ^^^^^^^ I wasn't expecting elements after this
Lists are immutable and singly-linked, so to append items to them
all the elements of a list would need to be copied into a new list.
This would be slow, so there is no built-in syntax for it.
Hint: Prepend items to the list and then reverse it once you are done.Il messaggio parla di liste singly-linked, “concatenate in un verso solo”. È il momento di vedere cosa vuol dire.
Dettagli nerd Com'è fatta una lista in memoria?
Una lista di Gleam è una catena di celle. Ogni cella contiene due cose: un elemento, e un riferimento alla cella successiva (l’ultima punta alla lista vuota). [18, 25, 30] è, in memoria, qualcosa come:
[18 | •]──►[25 | •]──►[30 | •]──►[]Da qui vengono tutte le regole di questa lezione:
- Aggiungere in testa costa pochissimo.
[10, ..scores]crea una sola cella nuova, con dentro 10 e un riferimento ascores. Le tre celle vecchie vengono condivise, non copiate: si può fare perché nessuno le cambierà mai. - Aggiungere in coda costa tanto. L’ultima cella punta a
[], e per farla puntare altrove bisognerebbe modificarla. Siccome non si può, tocca copiare tutte le celle. Con una lista di un milione di elementi, un milione di copie. - Arrivare al centesimo elemento richiede di seguire cento frecce. Non c’è modo di “saltare” direttamente in una posizione.
Questa struttura si chiama lista concatenata (linked list). Gli array di altri linguaggi sono invece un blocco unico di memoria, dove si salta in qualsiasi posizione con un calcolo; ma per aggiungere un elemento spesso bisogna spostare o copiare tutto il blocco.
Niente lista[0]
Proprio perché arrivare a una posizione vuol dire percorrere la catena, Gleam non ha una sintassi per prendere “l’elemento numero 3”. E attenzione, perché la sintassi degli altri linguaggi non dà nemmeno un errore:
import gleam/io
pub fn main() -> Nil {
let items = [10, 20, 30]
echo items[0]
io.println("Done")
}warning: Unused literal
┌─ /home/ada/learn-gleam/exercises/src/index.gleam:5:13
│
5 │ echo items[0]
│ ^^^ This value is never used
Hint: You can safely remove it.
src/index.gleam:5
[10, 20, 30]
DoneIl compilatore ha letto echo items e poi, separata, una lista [0] che nessuno usa. Stampa l’intera lista, e ti lascia solo un avviso. Se lanci gleam format lo vedi a colpo d’occhio: il formattatore divide la riga in due, echo items e sotto [0]. In Gleam le liste si usano in un altro modo: si guarda il primo elemento, e poi il resto. Lo vedremo nella prossima lezione, ed è più comodo di quanto sembri.
Quiz
Con let a = [2, 3], cosa vale [1, ..a], e cosa vale a dopo?
Le prime funzioni
Il modulo gleam/list della libreria standard ha decine di funzioni sulle liste. Eccone alcune che bastano per cominciare:
| Funzione | Cosa fa | Esempio |
|---|---|---|
list.length(l) | quanti elementi ci sono | list.length([3, 1, 2]) → 3 |
list.reverse(l) | la lista al contrario | list.reverse([3, 1, 2]) → [2, 1, 3] |
list.contains(l, x) | se x è nella lista | list.contains([3, 1, 2], 2) → True |
list.append(a, b) | gli elementi di a e poi quelli di b | list.append([1], [2, 3]) → [1, 2, 3] |
list.is_empty(l) | se la lista è vuota | list.is_empty([]) → True |
E == funziona anche con le liste: due liste sono uguali se hanno gli stessi elementi, nello stesso ordine.
Tieni presente il costo: list.length deve percorrere tutta la catena per contare, e list.append copia tutte le celle della prima lista. Vanno benissimo per liste di qualche migliaio di elementi, ma non conviene chiamarle mille volte di fila sulla stessa lista enorme.
Infine, gli elementi di una lista possono essere di qualsiasi tipo, anche altre liste: [[1, 2], [3]] ha tipo List(List(Int)).
Esercizio · sul tuo computer
La scaletta del concerto
Nel progetto exercises crea src/setlist.gleam. Parti dalla lista ["Intro", "Verse"], poi:
- crea una nuova lista con
"Soundcheck"in testa, usando i due puntini; - crea un’altra lista aggiungendo
["Outro"]in fondo, conlist.append; - stampa la lista finale con
string.inspect, poi il numero di brani, poi se contiene"Chorus", poi la lista al contrario.
L’output deve essere:
["Soundcheck", "Intro", "Verse", "Outro"]
Songs: 4
Has chorus: False
Reversed: ["Outro", "Verse", "Intro", "Soundcheck"](Attento al nome del file: un modulo che si chiama lists non compila, perché esiste già un modulo Erlang con quel nome.)
Mostra una soluzione (prima prova da solo!)
import gleam/bool
import gleam/int
import gleam/io
import gleam/list
import gleam/string
pub fn main() -> Nil {
let songs = ["Intro", "Verse"]
let songs = ["Soundcheck", ..songs]
let songs = list.append(songs, ["Outro"])
io.println(string.inspect(songs))
io.println("Songs: " <> int.to_string(list.length(songs)))
io.println("Has chorus: " <> bool.to_string(list.contains(songs, "Chorus")))
io.println("Reversed: " <> string.inspect(list.reverse(songs)))
}Tre let con lo stesso nome: ognuno crea una lista nuova, e il nome passa alla più recente. Nessuna lista è stata modificata.
Ricapitolando
- Una lista si scrive
[1, 2, 3];[]è la lista vuota. Il tipo èList(Int),List(String)… - Tutti gli elementi hanno lo stesso tipo.
string.inspect(valore)trasforma qualsiasi valore nel testo che lo rappresenta.[x, ..rest]crea una lista nuova conxin testa: costa pochissimo, erestnon cambia.- Non c’è una sintassi per aggiungere in coda:
list.appendesiste, ma copia la prima lista. - Una lista è una catena di celle: niente
lista[0](che compila, ma fa tutt’altro). list.length,list.reverse,list.contains,list.append,list.is_empty, e==tra liste.
Nella prossima lezione impariamo a smontare le liste con i pattern: il primo elemento, e il resto.