Lezione 6 di 7 · 18 min di lettura

Costruire il testo

Niente printf in Gleam: il testo si costruisce a pezzi. Il tubo |>, i decimali che non tornano, colonne allineate e sequenze di escape.

Il problema

Molti linguaggi hanno una funzione come printf o le f-string, dove scrivi un modello di testo con dei segnaposto e il linguaggio li riempie. Gleam no: in Gleam il testo si costruisce a pezzi, attaccando stringhe con <> e convertendo i numeri con le funzioni che hai già visto.

Funziona, ma con qualche numero in più la riga diventa un groviglio di parentesi:

gleam
  io.println(string.pad_start(int.to_string(total), 6, " "))

Per leggerla devi partire dal centro (total) e uscire verso l’esterno: prima int.to_string, poi string.pad_start, poi io.println. L’esatto contrario dell’ordine in cui scorre l’occhio. Gleam ha uno strumento apposta per questo.

Il tubo: |>

L’operatore |> si chiama pipe (tubo) e fa una cosa semplicissima: prende il valore a sinistra e lo passa come primo argomento alla funzione a destra.

gleam
  total |> int.to_string

è esattamente come scrivere int.to_string(total). E se la funzione ha altri argomenti, li scrivi normalmente: il valore che arriva dal tubo si mette al primo posto.

gleam
  "7" |> string.pad_start(3, "0")

equivale a string.pad_start("7", 3, "0"), e vale "007". La forza del tubo si vede quando i passaggi sono tanti: si mettono in fila, e si leggono nell’ordine in cui avvengono.

gleam
  total
  |> int.to_string
  |> string.pad_start(6, " ")
  |> io.println

“Prendi total, trasformalo in testo, allungalo a 6 caratteri, stampalo.” Stessa identica cosa della riga aggrovigliata di prima, ma si legge dall’alto in basso come una ricetta.

C’è un dettaglio di precedenza che torna utile: |> viene calcolato prima di <>. Quindi questa riga fa quello che speri, senza graffe:

gleam
  io.println("Total: " <> total |> int.to_string)

Prima total |> int.to_string, poi l’attacco con "Total: ".

Quiz

Cosa vale "gleam" |> string.uppercase |> string.pad_end(8, "!")?

Il caso dei decimali

float.to_string è comodo, ma ha le sue idee su come scrivere un numero. Guarda:

src/decimals.gleam
import gleam/float
import gleam/io

pub fn main() -> Nil {
  io.println(float.to_string(6.9))
  io.println(float.to_string(2.0))
  io.println(float.to_string(1250.0))
  io.println(float.to_string(1000.0))
  io.println(float.to_string(0.1 +. 0.2))
}
output
6.9
2.0
1250.0
1.0e3
0.30000000000000004

Tre sorprese in un colpo solo:

  • 6.9 non diventa 6.90: float.to_string non aggiunge zeri, quindi le colonne di prezzi non si allineano.
  • 1000.0 diventa 1.0e3, in notazione scientifica, mentre 1250.0 resta com’è. La regola è che viene scelta la scrittura più corta che rappresenta il numero esattamente: 1.0e3 ha un carattere in meno di 1000.0.
  • 0.1 +. 0.2 si porta dietro il suo piccolo errore, come abbiamo visto nella lezione precedente.

float.to_precision(x, 2) arrotonda un Float a due decimali (float.to_precision(3.14159, 2) vale 3.14), ma non risolve i primi due problemi: 6.9 resta 6.9.

Dettagli nerd Perché 0,1 + 0,2 non fa 0,3, e i centesimi sì?

Nei dettagli nerd della lezione 4 abbiamo visto che un Float è memorizzato in base 2, e che 0,1 in base 2 ha infinite cifre: il computer ne tiene 52 e butta il resto. Ogni volta che scrivi 0.1, in memoria c’è in realtà un numero come 0,1000000000000000055511151231257827. Sommandone due, i piccoli errori si sommano anche loro, e diventano visibili.

Gli Int non hanno questo problema: ogni intero è memorizzato esattamente. 690 centesimi sono 690, per sempre. Spostando la virgola di due posti (contando centesimi invece di euro) trasformi un problema di decimali approssimati in un problema di interi esatti.

Colonne allineate

Per stampare tabelle ordinate ti servono stringhe tutte della stessa lunghezza. Il modulo gleam/string ha tre funzioni perfette:

FunzioneCosa faEsempio
string.pad_start(s, n, c)aggiunge c all’inizio finché s è lunga nstring.pad_start("42", 5, " ") → " 42"
string.pad_end(s, n, c)aggiunge c alla fine finché s è lunga nstring.pad_end("Tea", 6, ".") → "Tea..."
string.repeat(s, n)ripete s per n voltestring.repeat("-", 5) → "-----"

Se la stringa è già lunga almeno n, pad_start e pad_end la lasciano com’è: non tagliano mai. E contano i caratteri come li vede una persona, quindi una è conta per uno, anche se pesa due byte.

Di solito i testi si allineano a sinistra (pad_end) e i numeri a destra (pad_start), così le unità finiscono una sotto l’altra:

src/table.gleam
import gleam/int
import gleam/io
import gleam/string

pub fn main() -> Nil {
  io.println(string.pad_end("Item", 10, " ") <> string.pad_start("Qty", 5, " "))
  io.println(string.repeat("=", 15))
  io.println(
    string.pad_end("Apples", 10, " ")
    <> string.pad_start(int.to_string(12), 5, " "),
  )
  io.println(
    string.pad_end("Pears", 10, " ")
    <> string.pad_start(int.to_string(7), 5, " "),
  )
  io.println(
    string.pad_end("Melons", 10, " ")
    <> string.pad_start(int.to_string(150), 5, " "),
  )
}
output
Item        Qty
===============
Apples       12
Pears         7
Melons      150

Caratteri speciali: le sequenze di escape

Come si mette una virgoletta dentro una stringa, se le virgolette la chiudono? Con una sequenza di escape: una barra rovesciata \ seguita da un carattere, che insieme significano “un carattere speciale”.

SequenzaSignificato
\"virgoletta doppia
\\barra rovesciata
\na capo (newline)
\ttabulazione
\rritorno carrello
\fsalto pagina
\u{...}un carattere Unicode dato il suo codice, per esempio \u{00E8} è è

E se ti serve un testo su più righe, puoi semplicemente andare a capo dentro la stringa: le stringhe di Gleam possono occupare più righe.

src/escapes.gleam
import gleam/io

pub fn main() -> Nil {
  io.println("She said: \"Hi!\"")
  io.println("Path: C:\\games\\gleam")
  io.println("One\nTwo")
  io.println("Col1\tCol2")
  io.println("Caff\u{00E8}")
  io.println(
    "Line A
Line B",
  )
}
output
She said: "Hi!"
Path: C:\games\gleam
One
Two
Col1	Col2
Caffè
Line A
Line B
Dettagli nerd Cos'è una tabulazione, e perché non allinea sempre?

\t non è un certo numero di spazi: è un carattere di controllo (il numero 9 nella tabella dei caratteri) che dice al terminale “salta alla prossima colonna di tabulazione”. Di solito le colonne sono ogni 8 caratteri. Per questo Col1\tCol2 sembra allineato, ma se il testo prima della tabulazione supera gli 8 caratteri, salta alla colonna successiva e l’allineamento va a monte. Per tabelle affidabili usa pad_start e pad_end: contano i caratteri, non le colonne del terminale.

Due uscite: stdout e stderr

Nella lezione 2 abbiamo visto che ogni programma ha due canali di uscita: lo standard output, per i risultati, e lo standard error, per i messaggi di errore e di diagnostica. io.println e io.print scrivono sul primo; io.println_error e io.print_error sul secondo. E anche echo scrive sullo standard error, perché è uno strumento di diagnostica.

Nel terminale finiscono tutti sullo schermo, e sembrano uguali. La differenza si vede quando redirigi l’uscita, per esempio per salvarla in un file:

terminale
gleam run -m report > report.txt

Il simbolo > manda lo standard output nel file report.txt, mentre lo standard error continua ad apparire sullo schermo (insieme alle righe Compiling e Running di gleam, che stanno anche loro sullo standard error). Così i messaggi di servizio non sporcano i risultati.

Quiz

Hai let cents = 1205. Quale espressione vale esattamente "12.05"?

Esercizio · sul tuo computer

Lo scontrino

Nel progetto exercises crea src/receipt.gleam. Un caffè costa 120 centesimi, un cornetto 130. Il cliente ha preso 2 caffè e 3 cornetti. Stampa esattamente:

output
Coffee x2 ......... 2.40
Croissant x3 ...... 3.90
------------------------
Total ............. 6.30

Regole:

  • tutti gli importi sono Int in centesimi, e vanno calcolati (prezzo per quantità, e la somma per il totale);
  • l’etichetta seguita da uno spazio ("Coffee x2 ") va allungata a 19 caratteri con dei punti, usando string.pad_end;
  • l’importo va allungato a 5 caratteri con degli spazi davanti, usando string.pad_start;
  • la riga di trattini è lunga 24 e si fa con string.repeat.

Ogni riga è lunga 24 caratteri: 19 per l’etichetta e 5 per l’importo. Per ora dovrai ripetere la conversione da centesimi a testo per ogni importo: nel prossimo modulo imparerai a scrivere funzioni tue, e ne basterà una.

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

pub fn main() -> Nil {
  let coffee = 120 * 2
  let croissant = 130 * 3
  let total = coffee + croissant

  let coffee_text =
    int.to_string(coffee / 100)
    <> "."
    <> { coffee % 100 |> int.to_string |> string.pad_start(2, "0") }
  let croissant_text =
    int.to_string(croissant / 100)
    <> "."
    <> { croissant % 100 |> int.to_string |> string.pad_start(2, "0") }
  let total_text =
    int.to_string(total / 100)
    <> "."
    <> { total % 100 |> int.to_string |> string.pad_start(2, "0") }

  io.println(
    string.pad_end("Coffee x2 ", 19, ".")
    <> string.pad_start(coffee_text, 5, " "),
  )
  io.println(
    string.pad_end("Croissant x3 ", 19, ".")
    <> string.pad_start(croissant_text, 5, " "),
  )
  io.println(string.repeat("-", 24))
  io.println(
    string.pad_end("Total ", 19, ".") <> string.pad_start(total_text, 5, " "),
  )
}

Ricapitolando

  • In Gleam non c’è printf: il testo si costruisce con <> e le funzioni di conversione.
  • x |> f(a) equivale a f(x, a): il tubo mette in fila le trasformazioni, nell’ordine in cui avvengono.
  • |> viene calcolato prima di <>: "Total: " <> n |> int.to_string funziona.
  • float.to_string sceglie la scrittura più corta: niente zeri finali, e a volte notazione scientifica (1.0e3).
  • Per i soldi: Int in centesimi, poi / 100, % 100 e string.pad_start(2, "0").
  • string.pad_start, string.pad_end e string.repeat per colonne e righe.
  • Escape: \", \\, \n, \t, \u{...}; le stringhe possono andare a capo.
  • io.println scrive sullo standard output; io.println_error ed echo sullo standard error.

Nella prossima lezione tiriamo le somme del modulo, con un riepilogo e una sfida finale da scrivere da zero.