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

Un'altra rappresentazione abbastanza nota è quella esponenziale.

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 in Rust    
  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);
}

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 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 è 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.
Apro una breve parentesi, che riguarda anche la convivenza tra numeri interi e con virgola riguardante gli 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 qualche 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 dice 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. 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 vediamo alcune funzioni, anche qui, 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.

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 numeri con virgola:

DIGITS           Numero delle cifre significative in base 10.
EPSILON          Valore di confronto minimo - Vedremo nel prossimo capitolo.
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 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: as è utile e comodo ma anche pericoloso.