Lezione 8 di 8 · 20 min di lettura

Riepilogo e sfida finale

Tutto il modulo in una pagina, un quiz di controllo e una macchina che dà il resto in monete, da scrivere da zero.

Guarda quanta strada

All’inizio del modulo i tuoi programmi andavano dritti dalla prima riga all’ultima, e ogni formula andava ricopiata. Adesso sai:

  • scrivere funzioni tue, con parametri, tipi, documentazione, pubbliche o private;
  • rendere le chiamate leggibili con le etichette, e scrivere funzioni che stanno bene nel tubo;
  • usare le funzioni come valori: passarle, restituirle, crearle al volo, catturarle con _;
  • far scegliere il programma con case, i pattern, i soggetti multipli e le guardie, con il compilatore che controlla i casi dimenticati;
  • ripetere con la ricorsione, e scriverla in coda con un accumulatore perché non consumi memoria.

Con questi strumenti, e senza nemmeno un if o un for, si può già scrivere qualsiasi calcolo. Il resto del corso aggiunge soprattutto modi migliori di organizzare i dati.

La pagina da tenere accanto

CosaCome si scrive
Definire una funzionefn area(width: Int, height: Int) -> Int { width * height }
Renderla pubblicapub fn area(...)
Documentarla/// Una riga di descrizione sopra la funzione
Etichettefn add_vat(cents: Int, rate rate: Int), chiamata add_vat(1000, rate: 22)
Scorciatoia delle etichettearea(width:, height:)
Tipo di una funzionefn(Int) -> Int
Funzione anonimafn(x) { x * 2 }
Catturaadd_vat(_, rate: 22), cioè fn(x) { add_vat(x, rate: 22) }
Variabile di tipofn twice(value: a, f: fn(a) -> a) -> a
Deciderecase x { 1 -> "one" _ -> "other" }
Sì o nocase age >= 18 { True -> ... False -> ... }
Pattern alternativi6 \| 7 -> "weekend"
Prefisso di stringa"/say " <> text -> text
Più soggetticase n % 3, n % 5 { 0, 0 -> ... }
Guardias if s >= 90 -> "A" (niente chiamate di funzione)
Ricorsionecaso base + passo ricorsivo più vicino al caso base
Ricorsione in codasum_loop(n - 1, total + n): la chiamata è l’ultima cosa
Nascondere l’accumulatorepub fn sum_to(n) chiama fn sum_to_loop(n, 0)
Fermare un programma bloccatoCtrl+C, poi a e Invio

Trovi tutto anche nel Codex (tasto K).

Quiz di controllo

Tre domande che mescolano tutto il modulo. Se ne sbagli una, la spiegazione ti dice in quale lezione tornare.

Quiz

Con fn twice(value: a, f: fn(a) -> a) -> a { f(f(value)) }, quanto vale twice(3, fn(x) { x * x })?

Quiz

Cosa restituisce label(15) con fn label(n: Int) -> String { case n { n if n > 10 -> "big" 15 -> "fifteen" _ -> "small" } }?

Quiz

Quale di queste funzioni ricorsive è scritta in coda?

La sfida: la macchina del resto

Ora un programma completo, da zero. È il distributore automatico che, dopo il pagamento, ti restituisce il resto in monete, sempre usando le monete più grandi possibili.

Esercizio · sul tuo computer

La macchina del resto

Nel progetto exercises crea src/change_machine.gleam. Un prodotto costa 1315 centesimi e il cliente paga con 2000. Il programma stampa il resto, poi le monete che lo compongono, dalla più grande alla più piccola (saltando quelle che non servono), e infine quante monete ha usato in tutto.

Le monete in euro valgono, in centesimi: 200, 100, 50, 20, 10, 5, 2, 1. Scrivi:

  1. format_cents(cents: Int) -> String, quella che conosci;
  2. next_coin(coin: Int) -> Int, che con un case dà la moneta successiva più piccola: next_coin(200) vale 100, next_coin(5) vale 2, e dopo l’ultima (1) restituisce 0;
  3. give_change(amount: Int) -> Int, pubblica, che stampa le righe delle monete e restituisce quante monete ha usato. Per farlo chiama una funzione privata ricorsiva in coda con due accumulatori, la moneta attuale (si parte da 200) e le monete usate finora (si parte da 0). Usa le etichette per renderne leggibile la chiamata.

Per ogni moneta, quante ne servono è amount / coin, e quello che resta da dare è amount % coin. Se una moneta non serve (ne servono 0), non si stampa niente. Quando la moneta attuale è 0, le monete sono finite e si restituisce il conteggio.

L’output deve essere:

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: nel passo ricorsivo, un blocco con tre cose: calcola quante monete servono con un let, stampa la riga oppure no con un case (ricordi? l’altro ramo vale Nil), poi chiamati con la moneta successiva.

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

pub fn main() -> Nil {
  let price = 1315
  let paid = 2000
  let change = paid - price

  io.println("Change: " <> format_cents(change))
  let coins = give_change(change)
  io.println("Coins: " <> int.to_string(coins))
}

/// Prints the coins that make up `amount`, largest first,
/// and returns how many coins were used.
pub fn give_change(amount: Int) -> Int {
  give_loop(amount, coin: 200, used: 0)
}

fn give_loop(amount: Int, coin coin: Int, used used: Int) -> Int {
  case coin {
    0 -> used
    _ -> {
      let count = amount / coin
      case count {
        0 -> Nil
        _ -> io.println(int.to_string(count) <> " x " <> format_cents(coin))
      }
      give_loop(amount % coin, coin: next_coin(coin), used: used + count)
    }
  }
}

/// The next smaller euro coin, or 0 after the smallest one.
fn next_coin(coin: Int) -> Int {
  case coin {
    200 -> 100
    100 -> 50
    50 -> 20
    20 -> 10
    10 -> 5
    5 -> 2
    2 -> 1
    _ -> 0
  }
}

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:

  • give_change è l’interfaccia pulita: un importo dentro, il numero di monete fuori. I due accumulatori e i loro valori iniziali restano un affare privato di give_loop.
  • give_loop è in coda: sia il resto (amount % coin) sia il conteggio (used + count) si calcolano negli argomenti.
  • Il case interno serve solo a stampare o no: il suo valore è sempre Nil, e il compilatore non protesta perché un Nil non usato non è un problema.
  • La sequenza delle monete sta tutta in next_coin. Se un giorno sparissero le monete da 1 e 2 centesimi, basterebbe cambiare quel case.

Cosa succede nel Modulo 3

Finora ogni nostro valore era da solo: un numero, un testo, un Bool. Le monete della sfida le abbiamo dovute mettere in fila con un trucco, una funzione che dà “la moneta dopo”. Nel prossimo modulo arrivano le liste: sequenze di valori, come [200, 100, 50], con i loro pattern ([first, ..rest]), la ricorsione che le percorre, e le funzioni della libreria standard che fanno la maggior parte del lavoro al posto tuo, come list.map e list.filter. E lì le funzioni come valori della lezione 3 diventeranno i tuoi attrezzi di tutti i giorni.