Lezione 6 di 6 · 18 min di lettura

Riepilogo e sfida finale

Il modulo sulle liste in una pagina, tre domande di controllo, e la macchina del resto riscritta con una lista di monete.

Guarda quanta strada

Cinque lezioni fa i tuoi valori erano tutti soli. Adesso sai:

  • creare liste, aggiungere in testa, e perché in coda no;
  • smontarle con i pattern [], [x], [first, ..rest], con il compilatore che conta le forme mancanti;
  • percorrerle con la ricorsione, e costruirne di nuove, anche in coda con list.reverse alla fine;
  • trasformarle, filtrarle e ordinarle con list.map, list.filter, list.sort e compagnia, tutto nel tubo;
  • ridurle a un valore solo con list.fold.

La pagina da tenere accanto

CosaCome si scrive
Una lista[1, 2, 3], tipo List(Int); [] è vuota
Aggiungere in testa[0, ..numbers]
Stampare una listastring.inspect(numbers)
Lista vuota / con un elemento[] -> ..., [x] -> ...
Il primo e il resto[first, ..rest] -> ...
Almeno due elementi[_, _, ..] -> ...
Nome a un sotto-pattern[[_, ..] as found, ..]
Ricorsione su una listacaso [], passo [first, ..rest]
Costruire in codaloop(rest, [new, ..acc]), poi list.reverse(acc)
Trasformarelist.map(l, fn(x) { ... })
Sceglierelist.filter(l, fn(x) { ... })
Ridurrelist.fold(over: l, from: 0, with: fn(acc, x) { ... })
Numerarelist.index_map(l, fn(x, i) { ... })
Ordinarelist.sort(l, by: int.compare)
Contare, qualcuno, tuttilist.count, list.any, list.all
Testo e listestring.split(s, on: " "), string.join(l, with: ", ")
Per ogni elementolist.each(l, io.println)

Trovi tutto anche nel Codex (tasto K).

Quiz di controllo

Quiz

Quanto vale list.map([1, 2, 3], fn(n) { n * n }) |> int.sum?

Quiz

Quale di queste clausole corrisponde alla lista [5]?

Quiz

Quanto vale list.fold(["a", "b"], "x", fn(acc, s) { acc <> s })?

La sfida: la macchina del resto, con le liste

Nella sfida del modulo 2 le monete erano messe in fila da una funzione, next_coin, che per ogni moneta diceva quale veniva dopo. Era un trucco: quella fila di monete è, in realtà, una lista. Riscriviamo la macchina del resto partendo proprio da lì.

Esercizio · sul tuo computer

La macchina del resto, versione 2

Nel progetto exercises crea src/change_list.gleam. Il prodotto costa 1315 centesimi e il cliente paga 2000, come prima. Le monete sono una costante:

gleam
const coins = [200, 100, 50, 20, 10, 5, 2, 1]

Scrivi:

  1. counts(amount: Int, coins: List(Int)) -> List(Int): quante monete servono di ogni tipo, nello stesso ordine della lista delle monete. Per 685 centesimi vale [3, 0, 1, 1, 1, 1, 0, 0]. È una ricorsione sulla lista delle monete: per la prima moneta ne servono amount / coin, e per le altre si ricomincia da amount % coin;
  2. lines(coins: List(Int), counts: List(Int)) -> List(String): una riga come "3 x 2.00" per ogni moneta usata, saltando quelle con conteggio 0. Percorre le due liste insieme, con un case su due soggetti;
  3. format_cents, come sempre.

In main stampa il resto, le righe (con list.each) e il totale delle monete (con int.sum sui conteggi):

output
Change: 6.85
3 x 2.00
1 x 0.50
1 x 0.20
1 x 0.10
1 x 0.05
Coins: 7

Suggerimento per lines: tre clausole. Tutte e due le liste hanno un primo elemento e il conteggio è 0; tutte e due hanno un primo elemento; tutto il resto (una delle due è finita).

Mostra una soluzione (prima prova da solo!)
src/change_list.gleam
import gleam/int
import gleam/io
import gleam/list
import gleam/string

const coins = [200, 100, 50, 20, 10, 5, 2, 1]

pub fn main() -> Nil {
  let change = 2000 - 1315
  let used = counts(change, coins)

  io.println("Change: " <> format_cents(change))
  lines(coins, used) |> list.each(io.println)
  io.println("Coins: " <> int.to_string(int.sum(used)))
}

/// How many of each coin make up `amount`, largest coins first.
fn counts(amount: Int, coins: List(Int)) -> List(Int) {
  case coins {
    [] -> []
    [coin, ..rest] -> [amount / coin, ..counts(amount % coin, rest)]
  }
}

/// One line per coin actually used, like "3 x 2.00".
fn lines(coins: List(Int), counts: List(Int)) -> List(String) {
  case coins, counts {
    [_, ..other_coins], [0, ..other_counts] -> lines(other_coins, other_counts)
    [coin, ..other_coins], [count, ..other_counts] -> [
      int.to_string(count) <> " x " <> format_cents(coin),
      ..lines(other_coins, other_counts)
    ]
    _, _ -> []
  }
}

fn format_cents(cents: Int) -> String {
  int.to_string(cents / 100)
  <> "."
  <> { cents % 100 |> int.to_string |> string.pad_start(2, "0") }
}

Qualche dettaglio da notare:

  • Il programma è diviso in tre passi indipendenti: calcolare i conteggi, trasformarli in righe, stampare. Ognuno si può provare da solo, e counts non stampa niente: restituisce solo dati.
  • Il totale delle monete non ha più bisogno di un accumulatore dedicato: è la somma dei conteggi.
  • counts e lines non sono in coda, e va bene così: le monete sono otto, e la forma “normale” è più facile da leggere.
  • Nella clausola dello zero la moneta non serve, quindi è _; il conteggio invece deve essere proprio 0.

Cosa succede nel Modulo 4

La macchina del resto ha un piccolo difetto: tiene le monete in una lista e i loro conteggi in un’altra, e tocca stare attenti a percorrerle insieme. Sarebbe più naturale avere una lista di coppie: moneta e conteggio, insieme. E più in generale: come si rappresenta un prodotto, che ha un nome, un prezzo e una quantità? O un semaforo, che può essere rosso, giallo o verde e nient’altro?

Nel prossimo modulo impareremo a costruire tipi nostri: le tuple per raggruppare valori al volo, i record con i campi con un nome, e i tipi personalizzati con le loro varianti, che con case diventano lo strumento più potente di tutto Gleam.