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 un Result e ricordarti che un dizionario non ha ordine;
  • contare e raggruppare con dict.upsert, list.group e dict.fold, e ordinare i risultati per valore;
  • usare Set quando 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

CosaCome si scrive
Un dizionariodict.from_list([#("Ada", 36)]), dict.new(); tipo Dict(String, Int)
Cercaredict.get(d, "Ada") → Ok(36) o Error(Nil)
Aggiungere / toglieredict.insert(d, k, v), dict.delete(d, k)
Aggiornare o inseriredict.upsert(d, k, fn(existing) { ... }), con Some/None
Riassumeredict.fold(d, 0, fn(acc, k, v) { ... })
Trasformare / filtraredict.map_values, dict.filter
Contenutodict.keys, dict.values, dict.to_list (senza ordine!)
Ordine decrescentelist.sort(l, fn(a, b) { int.compare(b.1, a.1) })
Paritàorder.break_tie(primo, secondo)
Raggrupparelist.group(l, fn(x) { chiave })
Un insiemeset.from_list(l), set.contains, set.insert
Operazioniset.union, set.intersection, set.difference
Un altro modulosrc/percent.gleam → import percent, percent.new(20)
Tipo opacopub opaque type Percent { Percent(value: Int) }
Importare un tipoimport 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 Inventory che avvolge un Dict(String, Int) (articolo → quantità);
  • un tipo di errore pubblico StockError, con InvalidQuantity, UnknownItem(String) e NotEnough(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 (con dict.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():

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

output
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 3
Mostra una soluzione (prima prova da solo!)
src/inventory.gleam
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) })
}
src/warehouse.gleam
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:

  • sell controlla tutto con un solo case sul risultato di dict.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 del fold: prova l’operazione, e in caso di errore restituisce il magazzino precedente, intatto. Niente è stato modificato, quindi non c’è niente da “annullare”.
  • warehouse non sa che dentro un Inventory c’è un dizionario. Se un giorno volessi tenere anche lo storico delle vendite, cambieresti solo inventory.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.