Lezione 6 di 7 · 20 min di lettura
Stampare e formattare
Costruire testo con +, String() e le t-string, arrotondare i decimali, allineare colonne, e scoprire perché in Mojo una stringa non ha una "lunghezza". Più le sequenze di escape e lo standard error.
Il problema
Alla fine della lezione scorsa il nostro programma stampava Energy: 0.23291666666666666 kWh. Corretto, ma nessuno vuole leggerlo. Stampare bene significa due cose: costruire il testo che vuoi, mettendo insieme parole e numeri, e impaginarlo, con i decimali giusti e le colonne allineate. Mojo ti dà diversi strumenti, ognuno con un suo scopo.
Tre modi per costruire una stringa
Il primo è la concatenazione con +, che però funziona solo tra stringhe. I numeri vanno convertiti con String(...):
def main():
var name = "Ada"
var age = 36
var line = "Name: " + name + ", age: " + String(age)
print(line)Name: Ada, age: 36Dimenticare la conversione è un errore di compilazione (invalid call to '__add__', con cannot be converted from 'Int'). Il secondo modo è passare più argomenti a String(...): li converte e li unisce tutti, senza spazi in mezzo, ed è più efficiente di una catena di + perché costruisce il risultato in un colpo solo.
var line = String("Name: ", name, ", age: ", age)Il terzo, il più comodo, sono le t-string (template string): una stringa preceduta da t, con le espressioni da inserire tra parentesi graffe.
def main():
var name = "Ada"
var age = 36
print(t"{name} is {age} years old, next year {age + 1}")
var width = 3.5
var height = 2.0
print(t"Area: {width * height} m2")
print(t"Literal braces: {{like this}}")Ada is 36 years old, next year 37
Area: 7.0 m2
Literal braces: {like this}Tra le graffe va qualsiasi espressione, e ogni valore viene convertito in testo da solo. Per scrivere una graffa vera si raddoppia: {{ e }}.
Dettagli nerd Perché una t-string è più veloce di una stringa normale?
Una t-string non è una String: è un valore di tipo TString che ricorda il modello e i valori da inserire, senza costruire subito il testo. print la scrive direttamente sull’output pezzo per pezzo, senza mai allocare in memoria la stringa completa. Allocare memoria (chiedere al sistema un blocco libero, e poi restituirlo) è tra le operazioni più costose di un programma che fa tanti piccoli lavori: evitarla dove non serve è il genere di dettaglio su cui Mojo è costruito. Se la stringa ti serve davvero, per esempio per salvarla in una variabile e modificarla, la costruisci esplicitamente con String(t"...").
Arrotondare
Niente specificatori di formato, quindi i decimali si sistemano prima di stamparli. Lo strumento è round, built-in come print:
def main():
var kwh = 0.23291666666666666
print(round(kwh, 2), round(kwh, 3))
var price = 2.4
var qty = 3.0
print(price * qty, round(price * qty, 2))0.23 0.233
7.199999999999999 7.2round(x, cifre) restituisce un altro Float64, arrotondato al numero di decimali richiesto, che print mostra nella forma più breve. Nella seconda riga vedi perché serve: 2,4 × 3 fa 7,199999999999999, perché 2,4 non è rappresentabile esattamente in binario.
C’è però un limite: round non aggiunge zeri. round(7.2, 2) resta 7.2, non 7.20: un Float64 non sa quante cifre vuoi vedere, conosce solo il suo valore.
Dettagli nerd Quanto fa round(2.5)?
Fa 2.0. E round(3.5) fa 4.0, round(0.5) fa 0.0, round(-2.5) fa -2.0. Senza cifre decimali, round usa l’arrotondamento al pari (round half to even, detto anche “arrotondamento del banchiere”): quando il valore è esattamente a metà, sceglie l’intero pari più vicino. Il motivo è statistico: arrotondare sempre per eccesso i casi a metà sposta la media verso l’alto, e su milioni di valori (pensa a una somma di importi, o a una media di misure) l’errore si accumula. Scegliendo il pari, metà delle volte si arrotonda per eccesso e metà per difetto. Python e lo standard IEEE 754 fanno lo stesso.
Soldi e decimali fissi
Quando i decimali devono essere esattamente due, come negli importi, la tecnica più robusta è non usare i decimali affatto: si lavora in centesimi, con gli Int, e si formatta solo alla fine. Con // e % separi euro e centesimi; per avere sempre due cifre (05, non 5) c’è ascii_rjust, che allinea a destra una stringa riempiendo a sinistra:
def main():
var cents = 1205
var euros = cents // 100
var rest = String(cents % 100).ascii_rjust(2, "0")
print(t"Total: {euros}.{rest} EUR")Total: 12.05 EURString(5).ascii_rjust(2, "0") fa "05": la stringa viene portata a 2 caratteri aggiungendo "0" a sinistra. Se è già lunga abbastanza, resta com’è.
Colonne allineate
Oltre a ascii_rjust esistono ascii_ljust (allinea a sinistra, riempiendo a destra) e ascii_center. Con queste si costruiscono tabelle leggibili:
def main():
print("Item".ascii_ljust(10), "Qty".ascii_rjust(5))
print("Tea".ascii_ljust(10, "."), String(2).ascii_rjust(5))
print("Biscuits".ascii_ljust(10, "."), String(12).ascii_rjust(5))
print("caffè".ascii_ljust(10, "."), String(3).ascii_rjust(5))Item Qty
Tea....... 2
Biscuits.. 12
caffè.... 3Guarda l’ultima riga: caffè ha un punto in meno e la colonna si è spostata. Non è un bug: il prefisso ascii_ è un avvertimento. Queste funzioni contano i byte, non i caratteri, e in UTF-8 la è occupa due byte. Con testo solo ASCII (lettere inglesi, cifre, punteggiatura) byte e caratteri coincidono; con gli accenti, no.
Una stringa non ha una lunghezza
Da qui nasce una delle regole più sorprendenti di Mojo. Prova a chiedere la lunghezza di una stringa come in Python:
def main():
var word = "caffè"
print(len(word))/home/ada/learn-mojo/length.mojo:3:11: error: `len(String/StringSlice)` is not supported because Mojo strings are UTF-8 encoded, so a single length is ambiguous: it could mean the number of UTF-8 bytes, the number of Unicode code points, or the number of user-visible characters (grapheme clusters). Use `s.byte_length()` or `len(s.bytes())` for the number of UTF-8 bytes, `len(s.codepoints())` for Unicode code points, or `len(s.graphemes())` for grapheme clusters.
print(len(word))
^~~
<unknown>:0: note: 'len' declared here
/home/ada/learn-mojo/.venv/bin/mojo: error: failed to parse the provided Mojo source moduleIl compilatore rifiuta la domanda perché è ambigua, e ti spiega le tre risposte possibili. Proviamole tutte, su due parole che a schermo sembrano quasi uguali:
def main():
var w = "caffè" # è is one code point, U+00E8
var v = "caffe\u0301" # e followed by a combining acute accent
print(w, w.byte_length(), w.count_codepoints(), w.count_graphemes())
print(v, v.byte_length(), v.count_codepoints(), v.count_graphemes())caffè 6 5 5
caffé 7 6 5byte_length(): quanti byte occupa in memoria. È quello che conta per le funzioniascii_, per i file e per la rete.count_codepoints(): quanti code point Unicode contiene.count_graphemes(): quanti caratteri percepiti da un essere umano (grapheme cluster).
La seconda parola (che a schermo appare come caffé) è scritta con una e seguita dal code point U+0301, l’accento acuto “combinante”, che si fonde con la lettera precedente: 7 byte, 6 code point, ma sempre 5 caratteri visibili. Per lo stesso motivo non puoi scrivere word[0]: devi dire se intendi il byte (word[byte=0]), il code point (word[codepoint=0]) o il carattere (word[grapheme=0]).
Dettagli nerd Byte, code point, grafemi: cos'è UTF-8?
Unicode assegna un numero, il code point, a ogni simbolo di ogni sistema di scrittura: A è U+0041, è è U+00E8, l’accento acuto combinante è U+0301. UTF-8 è il modo più diffuso di scrivere questi numeri in byte: i code point fino a U+007F (l’ASCII) occupano 1 byte, quelli fino a U+07FF 2 byte, poi 3 e fino a 4. Per questo è pesa 2 byte e il testo inglese non paga niente in più.
Un grafema è ciò che una persona chiamerebbe “un carattere”: a volte è un solo code point, a volte una sequenza (una lettera più accenti combinanti, una bandiera fatta di due code point, certi simboli composti anche da sette). Contare i grafemi richiede di applicare le regole dello standard Unicode (UAX #29), quindi è l’operazione più lenta delle tre; contare i byte è istantaneo. Mojo ti obbliga a scegliere perché la scelta ha un costo diverso, e perché la risposta “giusta” dipende da cosa devi fare.
Quiz
Hai var cents = 1205. Quale espressione produce esattamente il testo 12.05?
Caratteri speciali
Alcuni caratteri non si possono scrivere direttamente dentro le virgolette. Si usano le sequenze di escape, che iniziano con la barra rovesciata:
| Sequenza | Significato |
|---|---|
\n | a capo |
\t | tabulazione |
\" e \' | virgolette dentro una stringa |
\\ | una barra rovesciata |
\u00E8 | il code point U+00E8 (è), con 4 cifre esadecimali |
\U0001F525 | un code point oltre U+FFFF, con 8 cifre |
def main():
print("Line one\nLine two")
print("Name\tLanguage")
print('She said "hi"')
print('It\'s "fine"')
print("C:\\mojo\\learn")
print("Caff\u00e8 ready")
print(r"Raw: \n stays \n")
var note = """Triple quotes
keep the line breaks"""
print(note)Line one
Line two
Name Language
She said "hi"
It's "fine"
C:\mojo\learn
Caffè ready
Raw: \n stays \n
Triple quotes
keep the line breaksNota le virgolette: una stringa che contiene virgolette doppie si scrive più comodamente tra apici singoli, e viceversa. Quando servono entrambe, una delle due va protetta con la barra rovesciata. È anche la scelta che fa mojo format: tra "..." e '...' usa quella che richiede meno escape.
Il prefisso r crea una stringa grezza (raw), in cui le barre rovesciate restano barre rovesciate: comoda per percorsi Windows ed espressioni regolari. Le tre virgolette permettono una stringa su più righe, ed è la stessa sintassi delle docstring. E due letterali scritti uno accanto all’altro vengono uniti dal compilatore: "Hello, " "World" è "Hello, World".
Due uscite: stdout e stderr
print scrive sullo standard output, ma accetta un argomento file per scrivere altrove. Lo standard error si importa dal modulo sys:
from std.sys import stderr
def main():
print("Result: 42")
print("Warning: using default settings", file=stderr)Result: 42
Warning: using default settingsNel terminale le due righe appaiono insieme, ma sono canali diversi, e la shell sa separarli. Prova:
mojo streams.mojo > result.txtIl terminale mostra solo l’avviso: il risultato è finito nel file result.txt, perché > redirige solo lo standard output. È per questo che i messaggi diagnostici vanno su stderr: un altro programma può leggere il tuo output pulito, senza avvisi mescolati in mezzo. Il compilatore stesso scrive i suoi errori su stderr.
Esercizio · sul tuo computer
Lo scontrino
Crea receipt.mojo che stampi esattamente questo scontrino:
Coffee x2 ......... 2.40
Croissant x3 ...... 4.50
Water x1 .......... 0.90
Total ............. 7.80Regole:
- i prezzi unitari sono in centesimi: caffè
120, cornetto150, acqua90; - il totale va calcolato, non scritto a mano;
- la parte sinistra di ogni riga (
"Coffee x2 ", con lo spazio finale) va allineata a 19 caratteri conascii_ljust(19, "."), seguita da uno spazio e dall’importo; - gli importi si formattano con
//,%eascii_rjust(2, "0").
Senza funzioni (arrivano nel prossimo modulo) dovrai ripetere un po’ di codice: va bene così, ed è proprio il motivo per cui le funzioni esistono.
Mostra una soluzione (prima prova da solo!)
comptime COFFEE = 120
comptime CROISSANT = 150
comptime WATER = 90
def main():
var coffee = COFFEE * 2
var croissant = CROISSANT * 3
var water = WATER * 1
var total = coffee + croissant + water
var left = "Coffee x2 ".ascii_ljust(19, ".")
var cents = String(coffee % 100).ascii_rjust(2, "0")
print(t"{left} {coffee // 100}.{cents}")
left = "Croissant x3 ".ascii_ljust(19, ".")
cents = String(croissant % 100).ascii_rjust(2, "0")
print(t"{left} {croissant // 100}.{cents}")
left = "Water x1 ".ascii_ljust(19, ".")
cents = String(water % 100).ascii_rjust(2, "0")
print(t"{left} {water // 100}.{cents}")
left = "Total ".ascii_ljust(19, ".")
cents = String(total % 100).ascii_rjust(2, "0")
print(t"{left} {total // 100}.{cents}")Ricapitolando
+concatena solo stringhe: i numeri si convertono conString(n).String(a, b, c)unisce più valori in una volta.- Le t-string
t"... {espressione} ..."inseriscono valori senza allocare;{{e}}per le graffe; niente specificatori di formato. round(x, n)arrotonda (al pari quando è esattamente a metà) ma non aggiunge zeri; per gli importi si lavora in centesimi con//,%eascii_rjust(2, "0").ascii_ljust,ascii_rjust,ascii_centerallineano, contando byte.len()su una stringa non compila:byte_length(),count_codepoints(),count_graphemes(), e l’indicizzaziones[byte=i],s[codepoint=i],s[grapheme=i].- Escape:
\n,\t,\",\\,\u00E8; stringhe grezzer"..."e su più righe"""...""". print(..., file=stderr)confrom std.sys import stderrscrive sullo standard error.
Nella prossima lezione tiriamo le somme del modulo con una sfida che mette insieme tutto: costanti, conversioni, operatori e impaginazione.