Float
A floating-point number — a float — has a fractional part and an enormous range. The price is exactness: a float is stored in binary with a fixed number of significant digits, so most decimal fractions are kept as the nearest value it can represent, not the value you wrote.
Two everyday widths
| Type | Size | Significant digits | Literal |
|---|---|---|---|
float64 (also float) | 8 bytes | about 16 | 4.99, 384400.0 |
float32 | 4 bytes | about 7 | 0.75f32 |
A number with a decimal point is a float64 unless something says otherwise:
let price = 4.99;
let ratio: float = 0.75;
let distance: float64 = 384400.0;
A float32 value is written with the f32 suffix. Without it the literal is a float64 — and a float64 is never squeezed into a float32 behind your back, because digits would be lost:
let single: float32 = 0.75f32;
Six more widths
Everyday code uses the two above. Rux has six more, two narrower and four wider, each with its own suffix:
| Type | Size | Significant digits | Suffix |
|---|---|---|---|
float8 | 1 byte | about 1 | f8 |
float16 | 2 bytes | about 3 | f16 |
float80 | 16 bytes | about 19 | f80 |
float128 | 16 bytes | about 34 | f128 |
float256 | 32 bytes | about 71 | f256 |
float512 | 64 bytes | about 147 | f512 |
The narrow ones trade digits for space. They suit huge tables of approximate numbers — the weights of a neural network, say — where size matters more than any single value; a float8 cannot even count past 240. The wide ones keep far more digits than a float64, for work where rounding errors must stay out of sight. float80 is the x87 extended format: its 80 bits are stored in 16 bytes, so six of them sit unused.
Only float32 and float64 run on the processor's float instructions. The compiler computes the other six with integer code it generates, so they are slower. And {} does not print the four wide ones directly — finding their digits needs an allocator, which Floating-Point in the Rux Reference shows how to pass. Their sizes, like any other number, print fine:
PrintLine("float80 {} bits in {} bytes", float80::Bits, float80::Bytes);
Bits, Bytes and Max come from the Core package, so the lesson imports the types it asks about.
Precision you can see
A third has no exact binary form, so each type stops where its digits run out:
let third64 = 1.0 / 3.0;
let third32 = 1.0f32 / 3.0f32;
let third16 = 1.0f16 / 3.0f16;
let third8 = 1.0f8 / 3.0f8;
1/3 0.3333333333333333 as float64
1/3 0.33333334 as float32
1/3 0.3333 as float16
1/3 0.34 as float8
Each prints the shortest decimal that reads back as the same value at its own width. The float8 nearest to a third is really 0.34375 — its neighbours are 0.3125 and 0.375 — and 0.34 is the shortest decimal that still picks it out.
Why 0.1 + 0.2 is not 0.3
Neither 0.1 nor 0.2 is exact in binary, and their two tiny errors add up to a sum that is not quite 0.3:
let sum = 0.1 + 0.2;
0.1 + 0.2 0.30000000000000004
flowchart LR
a["`0.1` as written"] --> a2["stored as<br/>`0.1000000000000000055…`"]
b["`0.2` as written"] --> b2["stored as<br/>`0.2000000000000000111…`"]
a2 --> sum["sum ≈ `0.3000000000000000444…`"]
b2 --> sum
sum --> shown["printed as<br/>`0.30000000000000004`"]This is how floats behave in every language that uses them, not a Rux quirk. It is why money is usually counted in whole cents, with an integer.
Floats always look like floats
A float always prints with a decimal point, even when the value is whole, so it cannot be mistaken for an integer: let whole = 2.0; prints 2.0.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A floating-point number, or float, has a fractional part and an enormous range. The price is
// exactness: a float is stored in binary with a fixed number of significant digits, so most
// decimal fractions are kept as the nearest value it can represent, not the value written.
//
// Rux has eight float types. This lesson mostly uses `float64`, with about 16 significant digits,
// and `float32`, with about 7 in half the space. `float` names `float64`, which a number with a
// decimal point becomes when nothing says otherwise. Two narrower types, `float16` and `float8`,
// save space at the cost of digits, and four wider ones, `float80`, `float128`, `float256` and
// `float512`, keep far more digits than a `float64`.
import Core::{ float128, float16, float256, float512, float8, float80 };
import Io::PrintLine;
func Main() -> int {
// Three ways to end up with a float64.
let price = 4.99;
let ratio: float = 0.75;
let distance: float64 = 384400.0;
PrintLine("price {}", price);
PrintLine("ratio {}", ratio);
PrintLine("distance {}", distance);
// A float32 value is written with the `f32` suffix. Without it the literal is a float64, and
// a float64 is never squeezed into a float32 behind your back, because digits would be lost.
let single: float32 = 0.75f32;
PrintLine("single {}", single);
// The same division at both widths shows the difference in precision. A third has no exact
// binary form, so each type stops where its digits run out.
let third64 = 1.0 / 3.0;
let third32 = 1.0f32 / 3.0f32;
PrintLine("1/3 {} as float64", third64);
PrintLine("1/3 {} as float32", third32);
// `f16` and `f8` make the narrow types. A `float16` keeps about three digits, and a `float8`
// barely one: its largest value is 240. They suit huge tables of approximate numbers, such
// as the weights of a neural network, where size matters more than any single value.
let third16 = 1.0f16 / 3.0f16;
let third8 = 1.0f8 / 3.0f8;
PrintLine("1/3 {} as float16", third16);
PrintLine("1/3 {} as float8", third8);
PrintLine("largest {} as float16, {} as float8", float16::Max, float8::Max);
// The wide types have the suffixes `f80`, `f128`, `f256` and `f512`. Their arithmetic runs in
// code the compiler generates, so it is slower, and `{}` does not print them directly; the
// Rux Reference shows how to render one. Their sizes print like any other number, and show
// that a `float80` is stored in 16 bytes, with six of them unused.
PrintLine("float80 {} bits in {} bytes", float80::Bits, float80::Bytes);
PrintLine("float128 {} bits in {} bytes", float128::Bits, float128::Bytes);
PrintLine("float256 {} bits in {} bytes", float256::Bits, float256::Bytes);
PrintLine("float512 {} bits in {} bytes", float512::Bits, float512::Bytes);
// Neither 0.1 nor 0.2 is exact in binary either, and their two tiny errors add up to a sum
// that is not quite 0.3. This is how floats behave in every language that uses them, not a
// Rux quirk. It is why money is usually counted in whole cents with an integer.
let sum = 0.1 + 0.2;
PrintLine("0.1 + 0.2 {}", sum);
// A float always prints with a decimal point, even when the value is whole, so it cannot be
// mistaken for an integer.
let whole = 2.0;
PrintLine("whole {}", whole);
// A float and an integer are different types even when they hold the same number:
//
// let count: int = 2.0;
// error: cannot assign 'float64' to 'int'
//
// let single: float32 = 0.75;
// error: cannot assign 'float64' to 'float32'
return 0;
}
Besides Io, its Rux.toml lists Core under [Dependencies].
Run it
cd Examples/Basics/Float
rux run
price 4.99
ratio 0.75
distance 384400.0
single 0.75
1/3 0.3333333333333333 as float64
1/3 0.33333334 as float32
1/3 0.3333 as float16
1/3 0.34 as float8
largest 65500.0 as float16, 240.0 as float8
float80 80 bits in 16 bytes
float128 128 bits in 16 bytes
float256 256 bits in 32 bytes
float512 512 bits in 64 bytes
0.1 + 0.2 0.30000000000000004
whole 2.0
Common mistakes
Try it yourself
- Compute the area of a circle with radius
2.5, using3.14159for π. - Repeat the
1/3experiment with2.0 / 3.0. Where does each width round? - Add
0.1to avar total = 0.0;ten times and print it. Is it exactly1.0?
Learn more
float64andfloat32in the Rux Reference- Float special — infinity, NaN and negative zero
- Math — square roots, powers and trigonometry