Lezione 5 di 5 · 22 min di lettura
Riepilogo e sfida finale
Dizionari, insiemi e tipi opachi in una pagina, tre domande di controllo, e un magazzino che non può mai andare sotto zero.
Guarda quanta strada
In questo modulo i tuoi programmi hanno imparato a ritrovare le cose in fretta, e a proteggere i propri dati. Adesso sai:
- associare chiavi e valori con
Dict, cercarli con unResulte ricordarti che un dizionario non ha ordine; - contare e raggruppare con
dict.upsert,list.groupedict.fold, e ordinare i risultati per valore; - usare
Setquando conta solo se una cosa c’è, con unione, intersezione e differenza; - dividere un progetto in più moduli, e nascondere la forma dei dati dietro un tipo opaco e un costruttore intelligente.
La pagina da tenere accanto
| Cosa | Come si scrive |
|---|---|
| Un dizionario | dict.from_list([#("Ada", 36)]), dict.new(); tipo Dict(String, Int) |
| Cercare | dict.get(d, "Ada") → Ok(36) o Error(Nil) |
| Aggiungere / togliere | dict.insert(d, k, v), dict.delete(d, k) |
| Aggiornare o inserire | dict.upsert(d, k, fn(existing) { ... }), con Some/None |
| Riassumere | dict.fold(d, 0, fn(acc, k, v) { ... }) |
| Trasformare / filtrare | dict.map_values, dict.filter |
| Contenuto | dict.keys, dict.values, dict.to_list (senza ordine!) |
| Ordine decrescente | list.sort(l, fn(a, b) { int.compare(b.1, a.1) }) |
| Parità | order.break_tie(primo, secondo) |
| Raggruppare | list.group(l, fn(x) { chiave }) |
| Un insieme | set.from_list(l), set.contains, set.insert |
| Operazioni | set.union, set.intersection, set.difference |
| Un altro modulo | src/percent.gleam → import percent, percent.new(20) |
| Tipo opaco | pub opaque type Percent { Percent(value: Int) } |
| Importare un tipo | import percent.{type Percent} |
Trovi tutto anche nel Codex (tasto K).
Quiz di controllo
Quiz
Con let d = dict.from_list([#("a", 1), #("b", 2)]), cosa stampa io.println(string.inspect(dict.keys(d)))?
Quiz
Quanto vale set.to_list(set.difference(set.from_list([1, 2, 3]), set.from_list([2, 3, 4])))?
Quiz
Il modulo inventory definisce pub opaque type Inventory. Da un altro modulo, come si ottiene un Inventory?
La sfida: il magazzino
Un magazzino tiene il conto delle scorte di ogni articolo. Si possono rifornire articoli (anche nuovi) e vendere quelli presenti, ma mai più di quanti ce ne sono, e mai quantità nulle o negative. Il modulo che gestisce le scorte deve garantire queste regole da solo: nessun altro modulo deve poter creare un magazzino con quantità sbagliate.
Esercizio · sul tuo computer
Il magazzino
Nel progetto exercises crea due moduli.
src/inventory.gleam contiene:
- un tipo opaco
Inventoryche avvolge unDict(String, Int)(articolo → quantità); - un tipo di errore pubblico
StockError, conInvalidQuantity,UnknownItem(String)eNotEnough(item: String, available: Int); new() -> Inventory, un magazzino vuoto;restock(inventory, item, quantity) -> Result(Inventory, StockError): la quantità deve essere positiva; aggiunge alla quantità esistente o crea l’articolo (condict.upsert);sell(inventory, item, quantity) -> Result(Inventory, StockError): la quantità deve essere positiva, l’articolo deve esistere e averne abbastanza;to_list(inventory) -> List(#(String, Int)): le scorte, ordinate per nome.
src/warehouse.gleam definisce un tipo Operation con Restock(item: String, quantity: Int) e Sell(item: String, quantity: Int), ed esegue questa lista con un list.fold che parte da inventory.new():
let operations = [
Restock("apple", 10),
Sell("apple", 2),
Sell("pear", 1),
Restock("pear", 1),
Sell("pear", 3),
Restock("pear", 5),
Sell("pear", 3),
Sell("apple", 0),
]Per ogni operazione stampa cosa è stato fatto, oppure l’errore; un’operazione che fallisce lascia il magazzino com’era. Alla fine stampa le scorte:
Restocked 10 apple
Sold 2 apple
Error: unknown item pear
Restocked 1 pear
Error: not enough pear (1 available)
Restocked 5 pear
Sold 3 pear
Error: the quantity must be positive
Stock: apple 8, pear 3Mostra una soluzione (prima prova da solo!)
import gleam/dict.{type Dict}
import gleam/list
import gleam/option.{None, Some}
import gleam/string
/// The stock of a warehouse: how many of each item. Quantities are never negative.
pub opaque type Inventory {
Inventory(stock: Dict(String, Int))
}
pub type StockError {
InvalidQuantity
UnknownItem(String)
NotEnough(item: String, available: Int)
}
/// An empty warehouse.
pub fn new() -> Inventory {
Inventory(dict.new())
}
pub fn restock(
inventory: Inventory,
item: String,
quantity: Int,
) -> Result(Inventory, StockError) {
case quantity > 0 {
False -> Error(InvalidQuantity)
True ->
Ok(
Inventory(
dict.upsert(inventory.stock, item, fn(existing) {
case existing {
Some(available) -> available + quantity
None -> quantity
}
}),
),
)
}
}
pub fn sell(
inventory: Inventory,
item: String,
quantity: Int,
) -> Result(Inventory, StockError) {
case dict.get(inventory.stock, item) {
_ if quantity <= 0 -> Error(InvalidQuantity)
Error(Nil) -> Error(UnknownItem(item))
Ok(available) if available < quantity -> Error(NotEnough(item, available))
Ok(available) ->
Ok(Inventory(dict.insert(inventory.stock, item, available - quantity)))
}
}
/// The items in stock, sorted by name.
pub fn to_list(inventory: Inventory) -> List(#(String, Int)) {
inventory.stock
|> dict.to_list
|> list.sort(fn(a, b) { string.compare(a.0, b.0) })
}import gleam/int
import gleam/io
import gleam/list
import gleam/string
import inventory.{type Inventory, type StockError}
pub type Operation {
Restock(item: String, quantity: Int)
Sell(item: String, quantity: Int)
}
pub fn main() -> Nil {
let operations = [
Restock("apple", 10),
Sell("apple", 2),
Sell("pear", 1),
Restock("pear", 1),
Sell("pear", 3),
Restock("pear", 5),
Sell("pear", 3),
Sell("apple", 0),
]
let final = list.fold(operations, inventory.new(), apply)
io.println("Stock: " <> describe(final))
}
fn apply(stock: Inventory, operation: Operation) -> Inventory {
let result = case operation {
Restock(item:, quantity:) -> inventory.restock(stock, item, quantity)
Sell(item:, quantity:) -> inventory.sell(stock, item, quantity)
}
case result {
Ok(updated) -> {
io.println(done(operation))
updated
}
Error(error) -> {
io.println("Error: " <> explain(error))
stock
}
}
}
fn done(operation: Operation) -> String {
case operation {
Restock(item:, quantity:) ->
"Restocked " <> int.to_string(quantity) <> " " <> item
Sell(item:, quantity:) -> "Sold " <> int.to_string(quantity) <> " " <> item
}
}
fn explain(error: StockError) -> String {
case error {
inventory.InvalidQuantity -> "the quantity must be positive"
inventory.UnknownItem(item) -> "unknown item " <> item
inventory.NotEnough(item:, available:) ->
"not enough " <> item <> " (" <> int.to_string(available) <> " available)"
}
}
fn describe(stock: Inventory) -> String {
stock
|> inventory.to_list
|> list.map(fn(pair) { pair.0 <> " " <> int.to_string(pair.1) })
|> string.join(with: ", ")
}Qualche dettaglio da notare:
sellcontrolla tutto con un solocasesul risultato didict.get: la prima clausola,_ if quantity <= 0, usa una guardia su una variabile esterna al pattern, e scatta qualunque sia il risultato della ricerca.- In
warehouse,applyè la funzione delfold: prova l’operazione, e in caso di errore restituisce il magazzino precedente, intatto. Niente è stato modificato, quindi non c’è niente da “annullare”. warehousenon sa che dentro unInventoryc’è un dizionario. Se un giorno volessi tenere anche lo storico delle vendite, cambieresti soloinventory.gleam.- Gli errori si leggono con il prefisso del modulo (
inventory.NotEnough(item:, available:)), perchéStockErrorè pubblico e non opaco.
Cosa succede nel Modulo 7
Fino a qui ogni programma ha fatto una cosa alla volta, dall’inizio alla fine. Ma la BEAM, la macchina su cui gira Gleam, è stata costruita per fare tante cose contemporaneamente: milioni di piccoli processi indipendenti che si scambiano messaggi. È il segreto dei sistemi che non si fermano mai, come le centrali telefoniche per cui Erlang è nato.
Nel prossimo modulo faremo il primo passo in quel mondo: i processi, i messaggi tra processi, e i primi strumenti di OTP con le librerie gleam_erlang e gleam_otp. È la parte di Gleam che nessun altro linguaggio fa allo stesso modo.