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.

src/first_lists.gleam
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")
}
output
src/first_lists.gleam:7
[18, 25, 30]
src/first_lists.gleam:8
["Ada", "Grace", "Joe"]
src/first_lists.gleam:9
[]
Done

Una 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ì:

gleam
  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:

src/first_lists.gleam
import gleam/io

pub fn main() -> Nil {
  let items = [1, "two", 3]
  echo items
  io.println("Done")
}
output
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:

    String

Può 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:

gleam
  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:

src/prepend.gleam
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))
}
output
["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é:

src/prepend.gleam
import gleam/io
import gleam/string

pub fn main() -> Nil {
  let names = ["Grace", "Joe"]
  let all = [..names, "Ada"]
  io.println(string.inspect(all))
}
output
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:

output
[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 a scores. 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:

src/index.gleam
import gleam/io

pub fn main() -> Nil {
  let items = [10, 20, 30]
  echo items[0]
  io.println("Done")
}
output
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]
Done

Il 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:

FunzioneCosa faEsempio
list.length(l)quanti elementi ci sonolist.length([3, 1, 2]) → 3
list.reverse(l)la lista al contrariolist.reverse([3, 1, 2]) → [2, 1, 3]
list.contains(l, x)se x è nella listalist.contains([3, 1, 2], 2) → True
list.append(a, b)gli elementi di a e poi quelli di blist.append([1], [2, 3]) → [1, 2, 3]
list.is_empty(l)se la lista è vuotalist.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:

  1. crea una nuova lista con "Soundcheck" in testa, usando i due puntini;
  2. crea un’altra lista aggiungendo ["Outro"] in fondo, con list.append;
  3. 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:

output
["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!)
src/setlist.gleam
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 con x in testa: costa pochissimo, e rest non cambia.
  • Non c’è una sintassi per aggiungere in coda: list.append esiste, 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.