|
In Rust abbiamo due tipi di numeri con virgola, che differiscono per la precisione a cui possono arrivare:
let f1 = 2.0; let f2: f32 = 3.1; let f3 = 2f64; let f4 = 2.0_f32; quindi lasciando all'inferenza (che di default sceglie giustamente f64), oppure forzando noi il tipo o anche ponendo dei suffissi nelle due forme note. Un'altra rappresentazione abbastanza nota è quella esponenziale, attraverso la lettera "e" seguita dal numero dell'esponente che può essere positivo o negativo. fn main() { // Numeri molto grandi let large_number = 2.5e6; // equivalente a 2.5 * 10^6 println!("2.5e6 = {}", large_number); // Numeri molto piccoli let small_number = 3.2e-6; // equivalente a 3.2 * 10^-6 println!("3.2e-6 = {}", small_number); // Notazione esponenziale let number = 1e3; // equivalente a 1 * 10^3 println!("1e3 = {}", number); let negative_power = 4.12E-9; // equivalente a 4.12 * 10^-9 println!("4.12E-9 = {}", negative_power); } Per la magnitudine dei due tipi abbiamo anche in questo caso MIN e MAX. Nel seguente programma vedremo la differenza di precisione tra i due tipi ed eseguendolo potremo apprezzare la enorme capacità del tipo f64.
che presenta il seguente output per quanto riguarda la divisione (i limiti degli f32 e degli f64 li lascio visualizzare a voi che diversamente gli ultimi mi sfondano la tabella).
per le altre operazioni di base non ci sono differenze sostanziali ovviamente rispetto a quanto visto nel capitolo precedente, è intuitivo che non esista il resto della divisione. Per quanto riguarda l'occupazione in memoria dei due tipi vediamo il seguente semplice programma che vi potrà essere utile anche in altri casi: use std::mem; fn main() { let size_of_f64 = mem::size_of::<f64>(); let size_of_f32 = mem::size_of::<f32>(); println!("Dimensione di f64: {} byte", size_of_f64); println!("Dimensione di f32: {} byte", size_of_f32); } da cui risulterà che f64 occupa 8 byte ed f32 solo 4. Veniamo adesso ad un capitolo interessante, ovvero la convivenza tra numeri interi e numeri con virgola, delle cui peculiarità parleremo comunque più avanti in questo paragrafo. Le (piccole) difficoltà di questa convivenza sono prevedibili sulla base di quanto visto nel precedente paragrafo, dove addirittura non c'è convivenza pacifica tra interi di natura diversa, ma d'altronde la "safety" pretesa da Rust vuole anche qualche piccolo sacrificio, lo ribadiamo. Infatti, il seguente codice: let x = 7.0; let y = 3; let z = x + y; dà origine ad un errore: no implementation for `{float} + {integer}` e lo stesso vale se provate con altre operazioni, ovvero non possiamo lavorare con due tipi diversi. Quindi, armiamoci di pazienza e prepariamoci ad adottare qualche escamotage per risolvere la situazione. Una prima strada è quella di ricorrere all'operatore as, che già conosciamo: let x = 7.0; let y = 3; let z = x + y as f64; questo funziona e, in particolare, risulta utile in caso di divisioni quando volete trovare (eureka!) il risultato corretto. Si può anche fare il contrario, convertendo un float in intero, perdendo la parte decimale ovviamente: let x = 7.0123; let y = 5; let z = x as i32 + y; in dettaglio:
Come potrete facilmente intuire non è nemmeno possibile mischiare gli f32 con gli f64. Quindi: let x = 65f64; let y = 2_f32; println!("{}", x/y); Questo codice origina un errore nell'ambito del quale compare questa segnalazione: ^ no implementation for `f64 / f32` che è abbastanza chiaro e ribadisce quanto detto prima. Come detto possiamo ricorrere ad as ma con una possibile perdita di precisione se scegliamo di passare da f64 a 32 mentre passare fa f32 a f64 ha pure le sue peculiarità. Vedremo un paragrafo apposta devoluto alle conversioni numeriche. Per ora ci accontentiamo di as. Di nuovo con riferimento alla convivenza tra numeri interi e con virgola, apro una parentesi relativa agli elevamenti a potenza, un tipo di computazione che può essere molto utile e comune. Riassumendo: 1) base intera, esponente intero visto nel capitolo precedente, la funzione ad hoc è pow: let x = 7; let y = 3; let z = i32::pow(x, y); 2) base con virgola esponente intero: si usa powi let x = 7.17; let y = 3; let z = f64::powi(x, y); // si può usare anche f32::powi che ha un range meno ampio di valori ammissibili 3) base con virgola, esponente con virgola: si usa powf let x = 7.17; let y = 3.29; let z = f32::powf(x, y); 4) base intera esponente con virgola: si usa ancora powf con as per convertire il numero intero: let x = 7; let y = 3.29; let z = f32::powf(x as f32, y); Come gli interi, anche i numeri con virgola possono essere manipolati tramite numerose funzioni che trovate sul sito ufficiale. In questo paragrafo vedremo qualche esempio per impostarne l'utilizzo. Ne approfittiamo anche per vedere un aspetto interessante dell'inferenza e di come essa lavora. fn main() { let x = 2.34; println!("{}", x.trunc()); } questo codice non compila e il messaggio può essere abbastanza sorprendente: error[E0689]: can't call method `trunc` on ambiguous numeric type `{float}` --> r0098.rs:4:22 | 4 | println!("{}", x.trunc()); | ^^^^^ | help: you must specify a type for this binding, like `f32` | 3 | let x: f32 = 2.34; | ~~~~~~ Certo può sembrare e forse è, curioso che l'inferenza sia in grado di attribuire il tipo i32 alla variabile x e poi il metodo trunc (che come dice il suo nome tronca alla sola parte intera il numero con virgola) mi dica che il tipo è ambiguo. La spiegazione non è semplice, tanto che questo fatto, comune a tutte le funzioni, è oggetto di discussioni sui vari forum da parte di utenti un po' confusi (mi ci metto anche io) specie se abituati con altri linguaggi. Detto in breve, se è vero che il compilatore inferisce il tipo in fase di compilazione a garanzia della safety sui dati, tale meccanismo non interviene quando viene chiamato un metodo che si può in realtà applicare su più tipi e che può restituire a sua volta più tipi. Per il metodo insomma la variabile è sconosciuta pertanto deve essere specificata. L'errore si presenta anche in altri casi, vediamo ad esempio per gli interi:
println!("{}", x.abs());
Qui è il metodo abs che va in crisi in quanto non sa esattamente con quale tipo di variabile numerica abbia a che fare. In pratica il compilatore garantisce l'integrità dei dati in termini di tipologia ad essa assegnata così che l'uso che ne viene fatto sia coerente ma le singole funzioni necessitano diciamo di chiarimenti qualora possano lavorare su più tipi. Non ho approfondito il perchè di questa scelta ma esiste un capitolo molto interessante sul sito ufficiale legato a come lavora l'inferenza "under the hood". Una possibile soluzione per il codice precedente è riscrivere la riga incriminata come segue:
println!("{}", (x as f64).trunc());
Ad ogni modo trunc è solo delle tantissime funzioni che avete a disposizione. Nel seguente esempio ne vediamo altre, anche qui ovviamente si tratta di un campione non esaustivo ma sul sito ufficiale le trovate tutte.
Le righe 4, 6, 8, 10 ci fanno capire come funziona l'arrotondamento (round), che tende al numero più basso fino al valore.49999 e poi attonda verso l'alto. riga 11 - floor porta all'intero basso più vicino riga 12 - ceil porta all'intero alto più vicino riga 13 - trunc, tronca, come detto riga 14 - fract espone la parte frazionale del numero riga 15 - sqrt, la classica radice quadrata. Questa agisce sui numeri con virgola, non lavora sugli interi. Per i numeri con virgola sono disponibili anche i metodi is_finite(), is_infinite(), is_nan() che troveremo utili nel paragrafo dedicato agli overflow. Il programma successivo invece mostra l'uso di alcune funzioni built-in, qualcuna l'abbiamo già vista
Infine passiamo in rassegna un gruppo di interessanti costanti legate ai tipi dei numeri con virgola e che trovate già pronte per l'uso: DIGITS Numero delle cifre significative in base 10. EPSILON Valore di confronto minimo INFINITY Infinito (∞). MANTISSA_DIGITS Numero di cifre significative in base 2. MAX Valore float32 (o float64) massimo. MIN Valore float32 (o float64) minimo MIN_POSITIVE Più piccolo valore positivo. NAN Not a Number (NaN). NEG_INFINITY Infinito negativo (−∞). println!("{}", f32::DIGITS); println!("{}", f64::DIGITS); println!("{}", f32::EPSILON); mentre sono presenti alcune altre costanti numeriche che rappresentano valori di un certo uso comune: E Numero di Eulero e FRAC_1_PI 1/π FRAC_1_SQRT_2 1/sqrt(2) FRAC_2_PI 2/π FRAC_2_SQRT_PI 2/sqrt(π) FRAC_PI_2 π/2 FRAC_PI_3 π/3 FRAC_PI_4 π/4 FRAC_PI_6 π/6 FRAC_PI_8 π/8 LN_2 ln(2) LN_10 ln(10) LOG2_10 log2(10) LOG2_E log2(e) LOG10_2 log10(2) LOG10_E log10(e) PI il famoso pigreco SQRT_2 sqrt(2) TAU costante equivalente a 2 volte pigreco (τ) per richiamare queste ultime occorre appunto riferirsi al modulo consts: println!("{}", std::f32::consts::PI); Si tratta evidentemente di una raccolta molto utile in tante situazioni. Argomento interessante è quello del confronto tra le entità numeriche. Se per gli interi non ci sono particolari problemi la cosa cambia un po' quando si tratta di manipolare i numeri con virgola. Per riassumere un po' le cose ho dedicato ai confronti il paragrafo successivo a questo dove si parla anche delle conversioni. Per il momento ricordate ancora: as è utile e comodo ma anche pericoloso. Una questione spinosa quando si parla dei numeri con virgola, riguarda il loro confronto. La matematica in virgola mobile è un gran grutta bestia a volte, come dimostra il classico esempio:
fn main() {
let x = 0.1;
let y = 0.2;
let z = x + y;
println!("{}", z == 0.3);
}
ci restituisce false come risultato... ma perchè? Questo dipende dalla rappresentazione interna dei float (è una cosa comune a tutti i linguaggi di programmazione proprio per come i computer rappresentano i numeri float) ed è potenziale causa, come facilmente comprensibile, di molti problemi. Per rimediare, il primo classico sistema valido, in tutti i linguaggi è quello di confrontare i termini attraverso un valore piccolo a piacere. Quindi, usando ad esempio il nostro EPSILON appena visto qui sopra si potrebbe scrivere:
fn main() {
let x = 0.1;
let y = 0.2;
let z = x + y;
let c = (z - 0.3_f64).abs() > std::f64::EPSILON;
println!("{}", c);
}
che restituisce false perchè la distanza tra i due elementi di confronto è minore del nostro EPSILON. Quindi possiamo considerarli uguali (quali sono). E' possibile anche definire un epsilon personalizzato. Questa soluzione di norma funziona in maniera soddisfacente. Tuttavia Rust ci fornisce anche altre strade. Ricorrendo a Cargo per creare un nuovo progetto inseriamo nella sezione delle dipendenze, cargo.toml, la riga seguente che fa riferimento ad un crate: float-cmp = "0.9" e il codice potrà essere il seguente:
use float_cmp::approx_eq;
fn main() {
let a = 0.1 + 0.2;
let b = 0.3;
if approx_eq!(f64, a, b, ulps = 2) {
println!("a ≈ b");
} else {
println!("a ≠ b");
}
}
dove noterete senza dubbio quel ulps. ulps = 2 significa che due numeri in virgola mobile vengono considerati uguali se la loro distanza, espressa in ULP, è al massimo 2. ULP sta per Unit in the Last Place. Per farla breve:
Concludo infine il paragrafo con un programma, passatomi da un collega e trovato sulla rete, che può essere una comoda utility che potrete usare per verifica della uguaglianza, ma è più giusto dire "vicinanza" tra numeri con virgola.
pub fn float_eq(a: f64, b: f64) -> bool {
use std::f64;
let abs_a = a.abs();
let abs_b = b.abs();
let diff = (a - b).abs();
if a == b {
true
} else if a == 0.0 || b == 0.0 || diff > f64::EPSILON {
diff <= (abs_a + abs_b) * 1e-10
} else {
diff <= f64::EPSILON
}
}
fn main() {
let a = 0.1 + 0.2;
let b = 0.3;
if float_eq(a, b) {
println!("a ≈ b");
} else {
println!("a ≠ b");
}
}
|