Numbers · Lesson 16.2

Number limit

Source
Read a type's limits as associated constants — Min, Max, Bits, Lowest, Epsilon — and ask Core for them inside a generic function.
You'll need: Integer, Float, Const, Generic

How large can an int16 get? How many bytes does a uint8 take? What is the smallest step a float64 can take away from 1.0? You could look the answers up and type them into your program — or ask the type itself. Every number type carries facts about itself as associated constants, written with :: after the type's name: int16::Max, uint8::Bits, float64::Epsilon.

Using them instead of typed-in numbers makes code say what it means, and keeps it right if you later change a type.

Asking a type about itself

The constants are declared in Core, so the program imports each type it asks about and lists Core as a dependency:

import Core::{ MaximumOf, MinimumOf, float32, float64, int16, uint16, uint8 };

Every integer type answers the same four questions:

PrintLine("int16      {} to {}, {} bits in {} bytes", int16::Min, int16::Max, int16::Bits,
    int16::Bytes);
ConstantMeaningint16uint8
MinThe smallest value−327680
MaxThe largest value32767255
BitsThe width in bits168
BytesThe storage size, in bytes21

Because they are constants, they work anywhere a constant does — including inside another const:

const LargestPort: uint16 = uint16::Max;

A limit also makes a bounds check read like what it means. Will this reading fit in an int16?

let reading: int32 = 40000;
PrintLine("fits int16 {}", reading <= int16::Max && reading >= int16::Min);

It does not: the line prints false.

Limits inside a generic function

The constants belong to a concrete type. A generic function cannot reach them through its type parameter — T::Max is rejected, because the compiler does not know which type T will be. For that case Core offers MaximumOf and MinimumOf, which work the limit out from a sample value of the type:

func Span<T>(sample: T) -> T {
    return MaximumOf<T>(sample) - MinimumOf<T>(sample);
}

The sample's value does not matter, only its type. Span<uint8>(0) is 255 − 0 = 255.

Float limits

Floats have more limits than integers, because "smallest" means two different things for them:

PrintLine("float32    {} to {}", float32::Lowest, float32::Max);
PrintLine("float64    {} to {}", float64::Lowest, float64::Max);
PrintLine("smallest   {}", float64::MinPositive);
PrintLine("epsilon    {}", float64::Epsilon);
ConstantMeaningfloat64
LowestThe most negative finite value−1.7976931348623157e+308
MaxThe largest finite value1.7976931348623157e+308
MinPositiveThe smallest normal value above zero2.2250738585072014e-308
EpsilonThe gap between 1.0 and the next float up2.220446049250313e-16

There is no float64::Min: instead of one ambiguous name, the two meanings get a name each.

flowchart LR
    low["Lowest<br/>most negative"] --- neg["…"] --- mp["−MinPositive"] --- zero["0"] --- pos["MinPositive<br/>closest to zero"] --- more["…"] --- max["Max<br/>largest"]

Epsilon, the smallest step

Epsilon is how finely the numbers near 1.0 are divided. Add it to 1.0 and you get the next float up; add anything smaller and the sum rounds straight back to 1.0:

let one = 1.0;
PrintLine("1 + e      {}", one + float64::Epsilon);
PrintLine("1 + e / 2  {}", one + float64::Epsilon / 2.0);

The first prints 1.0000000000000002, the second 1.0 — half an epsilon was lost. That is why 0.1 + 0.2 is not exactly 0.3, as you saw in Float.

The program

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

Src/Main.rux
// Every number type carries facts about itself as associated constants, written with `::` after
// the type's name: `int16::Max`, `uint8::Bits`, `float64::Epsilon`. They are declared in `Core`,
// so a lesson that uses them imports each type it asks about and lists `Core` as a dependency.
//
// Because they are constants, they work anywhere a constant does, including inside another
// `const`. And because they belong to a concrete type, a generic function cannot reach them
// through its type parameter: `T::Max` is rejected with "'Max' not found in extend for type 'T'".
// For that case `Core` offers `MaximumOf` and `MinimumOf`, which work the limit out from a sample
// value of the type.
//
// Floats have more limits than integers, because "smallest" means two different things: `Lowest`
// is the most negative finite value, and `MinPositive` the smallest normal value above zero.
// `Epsilon` is the gap between 1.0 and the next float up.
import Core::{ MaximumOf, MinimumOf, float32, float64, int16, uint16, uint8 };
import Io::PrintLine;

// A limit can define another constant.
const LargestPort: uint16 = uint16::Max;

// A generic function asks `Core` for the limits of its own type parameter.
func Span<T>(sample: T) -> T {
    return MaximumOf<T>(sample) - MinimumOf<T>(sample);
}

func Main() -> int {
    // Integers: the range, and the storage behind it.
    PrintLine("int16      {} to {}, {} bits in {} bytes", int16::Min, int16::Max, int16::Bits,
        int16::Bytes);
    PrintLine("port       up to {}", LargestPort);
    PrintLine("uint8 span {}", Span<uint8>(0));

    // Floats: the finite range, and how finely it is divided.
    PrintLine("float32    {} to {}", float32::Lowest, float32::Max);
    PrintLine("float64    {} to {}", float64::Lowest, float64::Max);
    PrintLine("smallest   {}", float64::MinPositive);
    PrintLine("epsilon    {}", float64::Epsilon);

    // Epsilon is the smallest step 1.0 can take. Anything smaller added to 1.0 is lost.
    let one = 1.0;
    PrintLine("1 + e      {}", one + float64::Epsilon);
    PrintLine("1 + e / 2  {}", one + float64::Epsilon / 2.0);

    // A limit makes a bounds check read like what it means.
    let reading: int32 = 40000;
    PrintLine("fits int16 {}", reading <= int16::Max && reading >= int16::Min);
    return 0;
}

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

Run it

cd Examples/Numbers/NumberLimit
rux run
int16      -32768 to 32767, 16 bits in 2 bytes
port       up to 65535
uint8 span 255
float32    -3.4028235e+38 to 3.4028235e+38
float64    -1.7976931348623157e+308 to 1.7976931348623157e+308
smallest   2.2250738585072014e-308
epsilon    2.220446049250313e-16
1 + e      1.0000000000000002
1 + e / 2  1.0
fits int16 false

Common mistakes

Asking a float for Min.
float64::Min fails with error: 'Min' not found in extend for type 'float64'. Use Lowest for the most negative value, or MinPositive for the one closest to zero.
Asking a type parameter for a limit.
Inside func Top<T>(sample: T), T::Max fails with error: 'Max' not found in extend for type 'T'. Use MaximumOf<T>(sample) and MinimumOf<T>(sample).
Forgetting to import the type.
The constants live in Core. Without int16 in the import list, int16::Max fails with error: 'Max' not found in extend for type 'int16'.
Comparing with a limit of the other signedness.
reading <= uint8::Max with an int32 called reading fails with error: operator '<=' cannot compare left operand 'int32' with right operand 'uint8'. Convert the limit, which always fits: reading <= uint8::Max as int32.

Try it yourself

  1. Print Min, Max, Bits and Bytes for int8, uint32 and int64.
  2. Call Span<int8>(0). Predict the answer first — then explain the one you get.
  3. Print float32::Epsilon next to float64::Epsilon. How many more significant digits does a float64 keep?
  4. Change the bounds check so it asks whether reading fits in a uint16.

Learn more