Academy
Floating-Point Types, Literals, and the Prefix/Suffix System
Master floating-point precision in Rust with this overview of `f32` and `f64` types, literal syntax, and the complete prefix/suffix numeric system.
6 menit baca
Di halaman ini
If you've ever written 0.1 + 0.2 in any programming language and been surprised that the result is 0.30000000000000004, congratulations—you've discovered the fundamental nature of floating-point arithmetic. This isn't a bug in Rust, or JavaScript, or Python, or any language. It's a consequence of representing infinite decimal fractions in finite binary storage. The number 0.1 cannot be represented exactly in binary floating-point, just as 1/3 cannot be represented exactly in decimal. The approximation is incredibly close—close enough for virtually all practical purposes—but it's not exact.
Understanding floating-point arithmetic isn't optional for systems programmers. It's the difference between code that produces correct results and code that produces results that look correct but accumulate errors over millions of iterations until your financial system is off by thousands of dollars or your physics simulation has objects drifting through walls.
Rust provides two primary floating-point types—f32 and f64—with clear, predictable semantics based on IEEE 754. Beyond floating-point, Rust has a systematic approach to type literals through prefixes and suffixes that applies across all numeric types. Understanding this system makes numeric code clearer and more intentional.
At RantAI, where our AI models use floating-point for weights, gradients, and loss functions, and our scientific simulations accumulate millions of floating-point operations per timestep, understanding precision isn't academic—it's the difference between trustworthy results and numerical garbage. This article, drawn from Chapter 8, Sections 8.2.9, 8.2.10, and 8.3 of our guide "The Rust Programming Language," covers floating-point types, literal syntax, and the complete prefix/suffix system.
The Two Floating-Point Types#
fn main() {
// f64: double precision (the default) — ~15 decimal digits of precision
let pi: f64 = 3.141592653589793;
let avogadro: f64 = 6.022e23; // Scientific notation
let planck: f64 = 6.626e-34; // Very small number
// f32: single precision — ~7 decimal digits of precision
let pi_approx: f32 = 3.1415927; // Only 7 digits of precision
let gravity: f32 = 9.81;
// Default literal type is f64
let x = 1.0; // f64 (inferred)
let y = 2.5f32; // f32 (explicit suffix)
let z = 3.0f64; // f64 (explicit suffix, same as default)
// The classic floating-point surprise
println!("0.1 + 0.2 = {:.20}", 0.1_f64 + 0.2_f64);
// 0.30000000000000004441 — not exactly 0.3!
}
Use f64 by default. It provides approximately 15 decimal digits of precision with a range from ~5×10⁻³²⁴ to ~1.8×10³⁰⁸. On modern hardware, f64 operations are typically the same speed as f32 operations—the FPU processes both at the same rate. The extra precision costs nothing in performance and prevents accumulation errors.
Use f32 when memory bandwidth matters (GPU computation, large arrays of floats), when interfacing with APIs that expect 32-bit floats (OpenGL, some audio libraries), or when you're doing SIMD operations where f32 gives you twice as many lanes.
Literal Syntax: Scientific Notation and Suffixes#
fn main() {
// Standard decimal notation
let a = 1.0; // f64
let b = 0.5; // f64
let c = 100.0; // f64
// Scientific notation (e or E)
let d = 1.5e10; // 1.5 × 10¹⁰ = 15,000,000,000.0
let e = 2.998e8; // Speed of light in m/s (approximate)
let f = 1.6e-19; // Electron charge in coulombs
// Type suffixes
let g = 3.14f32; // Explicitly f32
let h = 2.718f64; // Explicitly f64
// Underscores for readability
let big = 1_000_000.000_001;
let sci = 6.022_140_76e23; // Avogadro's number
// Special values
let inf = f64::INFINITY;
let neg_inf = f64::NEG_INFINITY;
let nan = f64::NAN;
// NaN is weird: it's not equal to anything, including itself
println!("NaN == NaN: {}", nan == nan); // false!
println!("Is NaN: {}", nan.is_nan()); // true
}
Important syntax note: no spaces within a literal. 65.43e-21 is valid. 65.43 e-21 is a syntax error—Rust sees 65.43 and e as separate tokens.
The Complete Prefix/Suffix System#
Rust uses a systematic approach to numeric literals that applies across all types:
fn main() {
// Integer literal prefixes (base)
let decimal = 255; // No prefix = decimal (base 10)
let hex = 0xFF; // 0x = hexadecimal (base 16)
let octal = 0o377; // 0o = octal (base 8)
let binary = 0b1111_1111; // 0b = binary (base 2)
// Integer literal suffixes (type)
let a = 42i8; // i8
let b = 42i16; // i16
let c = 42i32; // i32 (default)
let d = 42i64; // i64
let e = 42i128; // i128
let f = 42u8; // u8
let g = 42u16; // u16
let h = 42u32; // u32
let i = 42u64; // u64
let j = 42u128; // u128
let k = 42usize; // usize
let l = 42isize; // isize
// Float literal suffixes
let m = 3.14f32; // f32
let n = 3.14f64; // f64 (default)
// Character/string literal prefixes
let ch = 'A'; // char (Unicode)
let byte = b'A'; // u8 (ASCII byte)
let string = "hello"; // &str (UTF-8)
let byte_string = b"hello"; // &[u8; 5] (byte string)
let raw = r"no \escapes"; // &str (raw, no escape processing)
let raw_bytes = br"raw bytes"; // &[u8] (raw byte string)
}
The system is consistent: prefixes specify the base or encoding, suffixes specify the type. Once you see the pattern, numeric literals in Rust are self-documenting—the literal itself tells you exactly what type and base it represents.
Floating-Point Best Practices#
fn main() {
// DON'T compare floats with ==
let a = 0.1 + 0.2;
let b = 0.3;
// if a == b { ... } // Unreliable!
// DO use epsilon comparison
let epsilon = 1e-10;
if (a - b).abs() < epsilon {
println!("Close enough to equal");
}
// DON'T use floats for money
// let price = 19.99f64; // Will accumulate rounding errors
// DO use integer cents
let price_cents: i64 = 1999; // $19.99 represented exactly
// Useful float methods
let x = -3.7f64;
println!("abs: {}", x.abs()); // 3.7
println!("floor: {}", x.floor()); // -4.0
println!("ceil: {}", x.ceil()); // -3.0
println!("round: {}", x.round()); // -4.0
println!("sqrt of 2: {}", 2.0f64.sqrt()); // 1.4142...
}
Broader Implications: Precision as Engineering#
At RantAI, floating-point precision isn't an afterthought—it's an engineering concern that's addressed in code review. Our ML training pipelines use f64 for loss accumulation (where precision matters over millions of iterations) and f32 for model weights (where memory matters for GPU batches). The type choice is documented, justified, and consistent.
Practical Applications & Strategic Takeaways#
For newcomers: Use f64 for everything. Switch to f32 only when an API requires it or when memory is a measured bottleneck. Never use floats for financial calculations.
For scientific programmers: Understand that f64 gives you about 15 significant digits. If your computation accumulates more than ~10¹⁵ operations, error analysis is essential.
For everyone: The prefix/suffix system is your friend. 0xFF_u8 is self-documenting: hexadecimal, 8-bit unsigned. No comment needed.
Our Commitment to Open Knowledge#
RantAI is committed to open education. Floating-point types and the literal system are covered in Chapter 8, Sections 8.2.9, 8.2.10, and 8.3 of our guide, "The Rust Programming Language," freely available online.
Explore these concepts further: https://trpl.rantai.dev
Support Our Mission & Get Your Handbook#
Get the Handbook on Amazon KDP: https://www.amazon.com/dp/B0DHCMD3F2
Get the Handbook on Google Play Books: https://play.google.com/store/books/details?id=INwfEQAAQBAJ
What's your worst floating-point precision story? The calculation that was wrong by just enough to matter? Share your numerical nightmares below!
#RustLang #FloatingPoint #IEEE754 #Literals #RantAI #LearnRust #Precision #NumericTypes #SystemsProgramming #TypeSafety
Artikel berikutnya
Integer Types and Literals: Choosing the Right Size for the Right Job