Numbers · Lesson 16.3

Float special

Source
See where infinity and NaN come from, why NaN == NaN is false, and how to test for them with IsNaN and IsInfinite.

An integer has nowhere to go past its limits: divide one by zero and the program stops. A float is different. It has a few values that are not ordinary numbers at all, and arithmetic that runs off the edge lands on one of them and carries on:

  • Infinity, Inf, when a result is too big to represent — or a non-zero number is divided by zero.
  • NaN, "not a number", when a result has no sensible value at all, such as zero divided by zero.

Neither is an error. The special value simply flows into everything computed from it. This lesson shows where they come from, and the one rule about them that catches everybody: how NaN compares.

Where the special values come from

let zero = 0.0;

PrintLine("1 / 0        {}", 1.0 / zero);
PrintLine("-1 / 0       {}", -1.0 / zero);
PrintLine("0 / 0        {}", zero / zero);
PrintLine("Max * 2      {}", float64::Max * 2.0);
PrintLine("Inf - Inf    {}", float64::Infinity - float64::Infinity);
ExpressionResultWhy
1.0 / 0.0InfThe quotient grows without bound
-1.0 / 0.0-InfThe same, below zero
0.0 / 0.0NaNNo single answer makes sense
float64::Max * 2.0InfToo large to represent: it overflows upwards
Inf - InfNaNAgain, no single answer

Both values are also available by name, float64::Infinity and float64::NaN, from Core. Formatting with a precision leaves them alone — there are no digits to round — so {:.3} still prints Inf and NaN.

Compare an integer: 10 / n with a zero n stops the program with Panic: division by zero and the line it happened on. Integers have no special values to fall back on.

Infinity compares like a number

Infinity is larger than every finite number, and equal to itself:

let inf = float64::Infinity;
PrintLine("Inf > Max    {}", inf > float64::Max);
PrintLine("Inf == Inf   {}", inf == inf);

Both lines print true. No surprises there.

NaN compares false with everything

NaN is unordered. Every comparison with it is false — even with itself — except !=, which is true:

let nan = float64::NaN;
PrintLine("NaN == NaN   {}", nan == nan);
PrintLine("NaN != NaN   {}", nan != nan);
PrintLine("NaN < 1      {}", nan < 1.0);
PrintLine("NaN > 1      {}", nan > 1.0);

NaN is neither less than 1 nor greater than it, nor equal to anything. The rule makes NaN spread instead of hiding: a test such as if total > limit cannot be fooled into true by a NaN total. But it also means a test written as x == float64::NaN can never succeed.

Asking what a value is

The reliable way to ask is a function from Core:

PrintLine("IsNaN        {}", IsNaN(nan));
PrintLine("IsInfinite   {}", IsInfinite(inf));
PrintLine("IsFinite     {}", IsFinite(float64::Max));
flowchart LR
    x["A float64 value"] --> n{"IsNaN?"}
    n -- "yes" --> isnan["NaN"]
    n -- "no" --> i{"IsInfinite?"}
    i -- "yes" --> isinf["Inf or -Inf"]
    i -- "no" --> fin["An ordinary number:<br/>IsFinite is true"]

IsFinite is true exactly when the value is neither NaN nor an infinity — the right check before you use a result as an ordinary number.

Two zeros

Zero has a sign too. -0.0 and 0.0 compare equal, yet dividing by them gives infinities of opposite signs:

let negativeZero = -0.0;
PrintLine("-0 == 0      {}", negativeZero == zero);
PrintLine("1 / -0       {}", 1.0 / negativeZero);

The first line prints true, the second -Inf. A negative zero turns up when a tiny negative result rounds to zero; it remembers which side it came from.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A float does not stop at its largest number. When a result is too big to represent, it becomes
// infinity, `Inf`; when a result has no sensible value at all, such as zero divided by zero, it
// becomes NaN, "not a number". Neither is an error: the arithmetic carries on and the special
// value flows into everything computed from it. Integers have neither, which is why an integer
// division by zero stops the program with "Panic: division by zero" and the line it happened on,
// while a float division by zero carries on.
//
// The surprise is in comparison. Infinity compares as you would expect, larger than every finite
// number. NaN compares false with everything, including itself, so `nan == nan` is `false` and
// `nan != nan` is `true`. A test written as `x == float64::NaN` can therefore never succeed; ask
// `Core::IsNaN` instead. Zero has a sign too: `-0.0 == 0.0`, yet dividing by them gives
// infinities of opposite signs.
import Core::{ IsFinite, IsInfinite, IsNaN, float64 };
import Io::PrintLine;

func Main() -> int {
    let zero = 0.0;

    // Where the special values come from.
    PrintLine("1 / 0        {}", 1.0 / zero);
    PrintLine("-1 / 0       {}", -1.0 / zero);
    PrintLine("0 / 0        {}", zero / zero);
    PrintLine("Max * 2      {}", float64::Max * 2.0);
    PrintLine("Inf - Inf    {}", float64::Infinity - float64::Infinity);

    // A precision has no digits to round here, so the words come out unchanged.
    PrintLine("rounded      {:.3} {:.3}", 1.0 / zero, zero / zero);

    // Infinity is ordered like a number.
    let inf = float64::Infinity;
    PrintLine("Inf > Max    {}", inf > float64::Max);
    PrintLine("Inf == Inf   {}", inf == inf);

    // NaN is unordered: every comparison with it is false, except `!=`.
    let nan = float64::NaN;
    PrintLine("NaN == NaN   {}", nan == nan);
    PrintLine("NaN != NaN   {}", nan != nan);
    PrintLine("NaN < 1      {}", nan < 1.0);
    PrintLine("NaN > 1      {}", nan > 1.0);

    // The reliable way to ask what a value is.
    PrintLine("IsNaN        {}", IsNaN(nan));
    PrintLine("IsInfinite   {}", IsInfinite(inf));
    PrintLine("IsFinite     {}", IsFinite(float64::Max));

    // Two zeros that are equal and still different.
    let negativeZero = -0.0;
    PrintLine("-0 == 0      {}", negativeZero == zero);
    PrintLine("1 / -0       {}", 1.0 / negativeZero);
    return 0;
}

Besides Io, its Rux.toml lists Core under [Dependencies].

Run it

cd Examples/Numbers/FloatSpecial
rux run
1 / 0        Inf
-1 / 0       -Inf
0 / 0        NaN
Max * 2      Inf
Inf - Inf    NaN
rounded      Inf NaN
Inf > Max    true
Inf == Inf   true
NaN == NaN   false
NaN != NaN   true
NaN < 1      false
NaN > 1      false
IsNaN        true
IsInfinite   true
IsFinite     true
-0 == 0      true
1 / -0       -Inf

Common mistakes

Testing for NaN with ==.
x == float64::NaN is always false, even when x is NaN — the compiler accepts it, and it simply never matches. Use IsNaN(x).
Expecting an error from a float division by zero.
1.0 / 0.0 is Inf, and the program carries on with it. Only an integer division by zero stops the program. Check a divisor that might be zero, or check the result with IsFinite.
Converting a special value to an integer.
as never fails, so it has to produce something: NaN as int32 is 0, and Inf as int32 saturates to 2147483647. Neither says anything went wrong. Checked convert reports it.

Try it yourself

  1. Compute float32::Max * 2.0f32. Is a float32 infinity printed any differently?
  2. Start from let nan = zero / zero; and work out nan + 1.0, nan * 0.0 and float64::Infinity * 0.0. Which are NaN?
  3. Write func SafeDivide(a: float64, b: float64) -> float64? that returns none when the result is not finite.
  4. Import IsNegativeZero from Core and use it to tell -0.0 from 0.0, which == cannot.

Learn more

  • float64 in the Rux Reference
  • Float — the float types and their precision
  • Number limit — Max, Lowest and Epsilon
  • Math — functions whose answers outside their domain are NaN and infinities