Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 6
Numeri con virgola

In Rust abbiamo due tipi di numeri con virgola, che differiscono per la precisione a cui possono arrivare:
  • f32 - definiti su 32 bit quindi singola precisione
  • f64 - definiti su 64 bit quindi doppia precisione
a parte la precisione e il "peso" in memoria i due tipi non hanno alcuna altra differenza sostanziale. Vediamo subito la dichiarazione che non presenta ovviamente novità rispetto agli interi:

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.

  Esempio 6.1
1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
    let f1: f32 = 7.0;
    let f2: f32 = 3.0;
    let f3: f64 = 7.0;
    let f4: f64 = 3.0;
    println!("{}", f1 / f2);
    println!("{}", f3 / f4);
    println!("{}", f32::MIN);
    println!("{}", f32::MAX);
    println!("{}", f64::MIN);
    println!("{}", f64::MAX);
}

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).

2.3333333
2.3333333333333335

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:

  Esempio 6.2
1
2
3
4
5
6
fn main(){
    let x1 = 7;
    let x2 = 3;
    println!("divisione intera: {}", x1 / x2);
    println!("divisione con virgola: {}", x1 as f64 / x2 as f64);
}

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:
let x = -2;
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.

  Esempio 6.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main()
{
    let x: f32 = 2.30;
    println!("{}", x.round());
    let y: f32 = 2.40;
    println!("{}", y.round());
    let z: f32 = 2.4999;
    println!("{}", z.round());
    let t: f32 = 2.50;
    println!("{}", t.round());
    println!("{}", t.floor());
    println!("{}", t.ceil());
    println!("{}", z.trunc());
    println!("{}", z.fract());
    println!("{}", z.sqrt());
}
 
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

  Esempio 6.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fn main() {
let x = 45.0_f64;

// Calcolo trigonometrico
let rad = x.to_radians(); // Converte gradi in radianti
let y_sin = rad.sin(); // Calcola il seno di x (x in radianti)
println!("Il seno di {} gradi è {}", x, y_sin);

// Calcolo del logaritmo
let val = 10.0_f64;
let log_base_10 = val.log(10.0); // Calcola il logaritmo in base 10
println!("Il logaritmo in base 10 di {} è {}", val, log_base_10);

// Esponenziale
let e_pow_x = val.exp(); // Calcola e^x
println!("e^{} = {}", val, e_pow_x);

// Radice quadrata
let sqrt_val = val.sqrt(); // Calcola la radice quadrata
println!("La radice quadrata di {} è {}", val, sqrt_val);

// Potenza
let pow_val = val.powf(2.0); // Eleva a potenza (in questo caso, al quadrato)
println!("{} al quadrato è {}", val, pow_val);
}

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:
  • 0 ULP → sono esattamente lo stesso valore binario

  • 1 ULP → differiscono di un solo gradino

  • 2 ULP → sono due gradini di differenza

  • ecc.

 
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");
    }
}