Lezione 3 di 6 · 18 min di lettura
Record
Varianti con dei dati dentro, e campi con un nome. Costruire un record, leggerne i campi, smontarlo con i pattern e crearne una copia aggiornata.
Una persona non è una tupla
Alla fine della lezione sulle tuple abbiamo lasciato un problema: #("Ada", 36, False) non dice cosa sia quel False. E se da qualche parte scrivessi #(36, "Ada", False), sarebbe un’altra cosa ancora, di un altro tipo. Per un gruppo di valori con un significato preciso, Gleam permette di dare un nome al gruppo e a ciascun valore.
Nella lezione precedente le varianti erano solo nomi. Ma una variante può anche contenere dei dati:
import gleam/io
pub type Person {
Person(name: String, age: Int, needs_glasses: Bool)
}
pub fn main() -> Nil {
let ada = Person("Ada", 36, False)
let grace = Person(name: "Grace", age: 85, needs_glasses: True)
echo ada
echo grace.name
echo grace.age
io.println("Done")
}src/people.gleam:10
Person("Ada", 36, False)
src/people.gleam:11
"Grace"
src/people.gleam:12
85
DonePerson è un tipo con una sola variante, che si chiama anch’essa Person (è l’abitudine quando c’è una variante sola, ma non è obbligatorio). La variante ha tre campi, ciascuno con un’etichetta e un tipo: name, age, needs_glasses. Un valore costruito così si chiama record.
Per costruirlo, la variante si usa come una funzione: Person("Ada", 36, False). E come per le funzioni, le etichette si possono scrivere nella chiamata, in qualsiasi ordine: Person(name: "Grace", age: 85, needs_glasses: True) si legge molto meglio, e di solito si preferisce questa forma. Se sbagli un tipo o dimentichi un campo, il compilatore ti ferma come con qualsiasi chiamata.
Leggere i campi
grace.name legge il campo name di grace: si chiama accessore. Se sbagli il nome del campo, il compilatore elenca quelli che esistono:
error: Unknown record field
┌─ /home/ada/learn-gleam/exercises/src/people.gleam:11:14
│
11 │ echo grace.nmae
│ ^^^^ Did you mean `name`?
The value being accessed has this type:
Person
It has these accessible fields:
.age
.name
.needs_glassesCon gli accessori, i record si combinano benissimo con le funzioni sulle liste:
people
|> list.filter(fn(person) { person.age >= 18 })
|> list.map(fn(person) { person.name })“Delle persone, tieni i maggiorenni, e prendine i nomi.” Senza nomi di campo, con le tuple, sarebbe stato person.1 >= 18 e person.0: funziona, ma bisogna ricordarsi cosa c’è in ogni posizione.
Smontare un record
Come le tuple, un record si smonta con un pattern, che ha la stessa forma del costruttore:
let Person(name: n, age: a, needs_glasses: _) = adaOra n vale "Ada" e a vale 36. Due scorciatoie rendono questi pattern molto più comodi:
..alla fine significa “gli altri campi non mi interessano”:Person(name: n, ..);- se vuoi chiamare la variabile come il campo, basta l’etichetta con i due punti, come la scorciatoia della lezione 2.2:
Person(name:, age:, ..)crea le variabilinameeage.
let Person(name:, age:, ..) = adaIl let funziona perché Person ha una sola variante: il pattern corrisponde sempre. I pattern sui record servono soprattutto dentro case, dove si combinano con valori esatti e guardie:
import gleam/io
pub type Person {
Person(name: String, age: Int, needs_glasses: Bool)
}
pub fn main() -> Nil {
io.println(greeting(Person(name: "Tim", age: 12, needs_glasses: False)))
io.println(greeting(Person(name: "Grace", age: 85, needs_glasses: True)))
io.println(greeting(Person(name: "Ada", age: 36, needs_glasses: False)))
}
fn greeting(person: Person) -> String {
case person {
Person(name:, age:, ..) if age < 18 -> "Hi " <> name <> "!"
Person(name:, needs_glasses: True, ..) ->
"Good day, " <> name <> " (large print)"
Person(name:, ..) -> "Good day, " <> name
}
}Hi Tim!
Good day, Grace (large print)
Good day, AdaPerson(name:, needs_glasses: True, ..) corrisponde solo alle persone con gli occhiali, e intanto prende il nome.
Quiz
Con let p = Person(name: "Ada", age: 36, needs_glasses: False), quale riga non compila?
Aggiornare, cioè copiare con modifiche
Un record non si modifica, ma capita spesso di volerne uno quasi uguale: la stessa persona con un anno in più. Si potrebbe ricostruirlo campo per campo, ma con tanti campi è scomodo. La sintassi di aggiornamento fa proprio questo:
let older = Person(..ada, age: 37)“Una Person con tutti i campi di ada, tranne age, che vale 37.” older è Person("Ada", 37, False), e ada è rimasta Person("Ada", 36, False). I due puntini sono gli stessi di [x, ..rest]: “tutto il resto viene da qui”.
Anche == funziona con i record: due record sono uguali se sono la stessa variante e tutti i campi sono uguali. Person(..ada, age: 37) == Person("Ada", 37, False) è True.
Dettagli nerd Come sta in memoria un record?
Sulla BEAM un record diventa una tupla di Erlang il cui primo elemento è l’atomo con il nome della variante (l’atomo della lezione precedente): Person("Ada", 36, False) è {person, <<"Ada">>, 36, false}. Le etichette non ci sono: esistono solo nel tuo codice e per il compilatore, che traduce ada.age in “prendi il terzo elemento della tupla”. Per questo le etichette non costano niente in memoria, ed è anche per questo che echo stampa Person("Ada", 36, False) senza nomi di campo.
E l’aggiornamento? Crea una tupla nuova, copiando i riferimenti ai valori dei campi. I valori stessi, come la stringa "Ada", non vengono copiati: vengono condivisi, perché nessuno potrà mai cambiarli.
Esercizio · sul tuo computer
Lo scontrino con i record
Nel progetto exercises crea src/receipt_items.gleam. Definisci un tipo:
pub type Item {
Item(name: String, price: Int, quantity: Int)
}con il prezzo unitario in centesimi. Il carrello è:
let cart = [
Item(name: "Coffee", price: 120, quantity: 2),
Item(name: "Croissant", price: 130, quantity: 3),
Item(name: "Juice", price: 250, quantity: 1),
]Scrivi subtotal(item: Item) -> Int (prezzo per quantità), riusa format_cents e la funzione receipt_line della lezione 2.1, e stampa lo scontrino: una riga per articolo ("Coffee x2" come etichetta), i trattini, e il totale calcolato con list.fold (o con list.map e int.sum).
Coffee x2 ......... 2.40
Croissant x3 ...... 3.90
Juice x1 .......... 2.50
------------------------
Total ............. 8.80Mostra una soluzione (prima prova da solo!)
import gleam/int
import gleam/io
import gleam/list
import gleam/string
pub type Item {
Item(name: String, price: Int, quantity: Int)
}
pub fn main() -> Nil {
let cart = [
Item(name: "Coffee", price: 120, quantity: 2),
Item(name: "Croissant", price: 130, quantity: 3),
Item(name: "Juice", price: 250, quantity: 1),
]
cart
|> list.each(fn(item) {
let label = item.name <> " x" <> int.to_string(item.quantity)
io.println(receipt_line(label, subtotal(item)))
})
let total = cart |> list.map(subtotal) |> int.sum
io.println(string.repeat("-", 24))
io.println(receipt_line("Total", total))
}
fn subtotal(item: Item) -> Int {
item.price * item.quantity
}
fn format_cents(cents: Int) -> String {
int.to_string(cents / 100)
<> "."
<> { cents % 100 |> int.to_string |> string.pad_start(2, "0") }
}
fn receipt_line(label: String, cents: Int) -> String {
string.pad_end(label <> " ", 19, ".")
<> string.pad_start(format_cents(cents), 5, " ")
}list.map(subtotal) passa la funzione subtotal così com’è: riceve un Item e restituisce un Int, esattamente quello che serve. Confronta con lo scontrino del modulo 1: là c’erano due prodotti scritti a mano, qui c’è un carrello di dati, e aggiungere un articolo vuol dire aggiungere una riga alla lista.
Ricapitolando
- Una variante può contenere dati:
Person(name: String, age: Int). Un valore così è un record. - Con una sola variante, di solito si chiama come il tipo.
- Si costruisce come una chiamata:
Person("Ada", 36)o, meglio,Person(name: "Ada", age: 36). record.campolegge un campo (accessore); un campo sbagliato non compila.- Si smonta con un pattern:
Person(name:, age:, ..), conletse il tipo ha una sola variante, altrimenti in uncase. Person(..ada, age: 37)crea un record nuovo, uguale tranne i campi indicati.- I record si confrontano con
==, campo per campo.
Nella prossima lezione mettiamo insieme le due idee: un tipo con più varianti, ciascuna con i suoi dati.