Number limit
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);
| Constant | Meaning | int16 | uint8 |
|---|---|---|---|
Min | The smallest value | −32768 | 0 |
Max | The largest value | 32767 | 255 |
Bits | The width in bits | 16 | 8 |
Bytes | The storage size, in bytes | 2 | 1 |
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);
| Constant | Meaning | float64 |
|---|---|---|
Lowest | The most negative finite value | −1.7976931348623157e+308 |
Max | The largest finite value | 1.7976931348623157e+308 |
MinPositive | The smallest normal value above zero | 2.2250738585072014e-308 |
Epsilon | The gap between 1.0 and the next float up | 2.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.
// 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
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.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).The constants live in
Core. Without int16 in the import list, int16::Max fails with error: 'Max' not found in extend for type 'int16'.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
- Print
Min,Max,BitsandBytesforint8,uint32andint64. - Call
Span<int8>(0). Predict the answer first — then explain the one you get. - Print
float32::Epsilonnext tofloat64::Epsilon. How many more significant digits does afloat64keep? - Change the bounds check so it asks whether
readingfits in auint16.
Learn more
- The primitive types and int16 in the Rux Reference
- Integer and Float — the types these limits describe
- Float special — what lies beyond
Max: infinity and NaN - Generic — functions with type parameters
16.1 Wide integer
Count past 64 bits with int128, int256 and int512 and their unsigned twins: wide literals, silent widening, explicit narrowing, and the same wrap-around at the edges.
16.3 Float special
See where infinity and NaN come from, why NaN == NaN is false, and how to test for them with IsNaN and IsInfinite.