Basics · Lesson 1.6

Float

Source
Store fractional numbers in float64, float32 and the float alias, meet the narrow and wide widths from float8 to float512, and see why a float is only ever an approximation.
You'll need: Variable, Integer

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

TypeSizeSignificant digitsLiteral
float64 (also float)8 bytesabout 164.99, 384400.0
float324 bytesabout 70.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:

TypeSizeSignificant digitsSuffix
float81 byteabout 1f8
float162 bytesabout 3f16
float8016 bytesabout 19f80
float12816 bytesabout 34f128
float25632 bytesabout 71f256
float51264 bytesabout 147f512

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.

Src/Main.rux
// 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

  1. Compute the area of a circle with radius 2.5, using 3.14159 for π.
  2. Repeat the 1/3 experiment with 2.0 / 3.0. Where does each width round?
  3. Add 0.1 to a var total = 0.0; ten times and print it. Is it exactly 1.0?

Learn more