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:
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.
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.
"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.
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:
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:
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))
}6.9
2.0
1250.0
1.0e3
0.30000000000000004Tre sorprese in un colpo solo:
6.9non diventa6.90:float.to_stringnon aggiunge zeri, quindi le colonne di prezzi non si allineano.1000.0diventa1.0e3, in notazione scientifica, mentre1250.0resta com’è. La regola è che viene scelta la scrittura più corta che rappresenta il numero esattamente:1.0e3ha un carattere in meno di1000.0.0.1 +. 0.2si 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:
| Funzione | Cosa fa | Esempio |
|---|---|---|
string.pad_start(s, n, c) | aggiunge c all’inizio finché s è lunga n | string.pad_start("42", 5, " ") → " 42" |
string.pad_end(s, n, c) | aggiunge c alla fine finché s è lunga n | string.pad_end("Tea", 6, ".") → "Tea..." |
string.repeat(s, n) | ripete s per n volte | string.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:
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, " "),
)
}Item Qty
===============
Apples 12
Pears 7
Melons 150Caratteri 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”.
| Sequenza | Significato |
|---|---|
\" | virgoletta doppia |
\\ | barra rovesciata |
\n | a capo (newline) |
\t | tabulazione |
\r | ritorno carrello |
\f | salto 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.
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",
)
}She said: "Hi!"
Path: C:\games\gleam
One
Two
Col1 Col2
Caffè
Line A
Line BDettagli 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:
gleam run -m report > report.txtIl 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:
Coffee x2 ......... 2.40
Croissant x3 ...... 3.90
------------------------
Total ............. 6.30Regole:
- tutti gli importi sono
Intin 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, usandostring.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!)
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 af(x, a): il tubo mette in fila le trasformazioni, nell’ordine in cui avvengono.|>viene calcolato prima di<>:"Total: " <> n |> int.to_stringfunziona.float.to_stringsceglie la scrittura più corta: niente zeri finali, e a volte notazione scientifica (1.0e3).- Per i soldi:
Intin centesimi, poi/ 100,% 100estring.pad_start(2, "0"). string.pad_start,string.pad_endestring.repeatper colonne e righe.- Escape:
\",\\,\n,\t,\u{...}; le stringhe possono andare a capo. io.printlnscrive sullo standard output;io.println_erroredechosullo standard error.
Nella prossima lezione tiriamo le somme del modulo, con un riepilogo e una sfida finale da scrivere da zero.