std/number

Every numeric type of the language, the traits their arithmetic and bit operations go through, Real, and ByteSize.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

Die generierte Referenz von std/number zeigt jede öffentliche Deklaration mit Signatur, Doc-Kommentar und Beispielen.

std/number declares every numeric type, the traits their operators go through (Numeric, Signed, Integer, Bits, Real), the two error types a numeric conversion fails with, and ByteSize, a number of bytes in its units. Every name below is already in scope through the prelude.

Import#

use Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 from "std/number"
use Float32, Float64, Numeric, Signed, Bits from "std/number"
const value: Int8 = 100
print value.absolute()

Declarations#

Numeric, Signed, Bits, Real#

trait Numeric with Add, Subtract, Multiply, Divide, Remainder, Equals, Compare, Show, TryFrom<String, NumberParseError> {
  static zero: Self
  static one: Self
}
trait Signed with Numeric, Negate {
  fn absolute(): Self
}
trait Bits {
  fn bitwiseAnd(other: Self): Self
  fn bitwiseOr(other: Self): Self
  fn bitwiseExclusiveOr(other: Self): Self
  fn bitwiseNot(): Self
  fn shiftedLeft(by: Int64): Self
  fn shiftedRight(by: Int64): Self
}

Numeric is the arithmetic and comparison every number has; Signed adds absolute() and the unary -. The bit operators &, |, ^, ~, << and >> are the members of Bits - & binds like *, | and ^ like +, a shift between * and ** - and the members stay the named form, so masking and shifting are named methods on Bits instead: value.bitwiseAnd(0xFF), seed.bitwiseExclusiveOr(byte).shiftedLeft(by: 5). The shift of a signed type is arithmetic (it keeps the sign), the shift of an unsigned type is logical, and a shift by a negative amount or by the width of the type or more panics, like every other operation that leaves its range.

trait Real with Signed, Power, Power<Int64> {
  static pi: Self
  static tau: Self
  static e: Self
  static epsilon: Self
  fn squareRoot(): Self
  fn sine(): Self
  fn cosine(): Self
  fn tangent(): Self
  fn arcSine(): Self
  fn arcCosine(): Self
  fn arcTangent(): Self
  fn arcTangentDivided(by: Self): Self
  fn exponential(): Self
  fn naturalLogarithm(): Self
  fn logarithm(base: Self): Self
  fn floor(): Self
  fn ceiling(): Self
  fn round(): Self
  fn halved(): Self
  fn doubled(): Self
  fn radiansOfDegrees(): Self
  fn degreesOfRadians(): Self
}

Real is Signed plus everything that needs a root or an angle, and it is the bound a library that is generic over its scalar separates its two halves along: dot and lengthSquared are arithmetic and need only Numeric, while length, normalized and every rotation need a Real. Angles are in radians throughout, and arcTangentDivided(by:) is the two-argument arc tangent that uses the sign of both to pick the quadrant - it takes a label because the order of the two is the one thing everybody gets wrong.

Two implementors. Float64 is the fast one and answers whatever the platform's mathematics library answers, which is not the same bits on every machine. Fixed (std/linear) is the deterministic one: every operation on it, the square root and the trigonometry included, is integer arithmetic, so a lockstep simulation and a replay run on it.

Real is also the scalar that has a power by a value of its own kind and by an Int (x ** 0.5, x ** 3), an exponential(), a naturalLogarithm() and a logarithm(base:) - so a body that is generic over its scalar writes (Scalar.e ** exponent) or value.logarithm(base: two) exactly as a Float64 body does, and a Fixed answers the same bits on every machine.

pi, tau, e and epsilon are required constants: every implementor carries them under those names (Float64.pi, Fixed.tau, Float.e), and a body that is generic over its scalar reads them through its parameter - Scalar.pi - where it could not write the digits. epsilon is the gap between one and the next value the scalar holds: the machine epsilon of a Float64, one part of a Fixed. The one such a body needs is Scalar.one, which every Numeric has, and halved and doubled are the two pieces of arithmetic it would otherwise write with a literal.

Integer#

trait Integer with Numeric {
  static minimum: Self
  static maximum: Self
  fn addedChecked(other: Self): Self?
  fn subtractedChecked(other: Self): Self?
  fn multipliedChecked(other: Self): Self?
  fn dividedChecked(other: Self): Self?
  fn remainderChecked(other: Self): Self?
}

Every integer type, Int8 to UInt64, is an Integer: a Numeric with a range, and the total twins of its operators. a.addedChecked(b) is a + b where the sum fits the type and None where + would panic, and the same for -, *, / and % - the division and the remainder answer None for a zero divisor and for the smallest value of a signed type divided by -1 as well. All five are defaults of the trait, written over the operators with a comparison in front, so a body that is generic over Value: Integer has them too.

print Int.maximum.multipliedChecked(2)
print(100.subtractedChecked(1) ?? 0)
// prints None
// prints 99

NumberParseError, NumberRangeError#

type NumberParseError with Error {
  text: String
}
type NumberRangeError with Error {
  message: String
}

What a conversion of a number fails with: NumberParseError where text is not a number, NumberRangeError where a value does not fit the target width.

The integer and floating-point types#

native type Int8 with Signed, Hash, Bits, Power<Int64>
extend Int8 {
  static zero: Self = 0
  static one: Self = 1
  static minimum: Self = -128
  static maximum: Self = 127
}
extend Int8 with TryFrom<String, NumberParseError>
extend Int8 with TryFrom<Int64, NumberRangeError>
native type Int16 with Signed, Hash, Bits, Power<Int64>
extend Int16 {
  static zero: Self = 0
  static one: Self = 1
  static minimum: Self = -32768
  static maximum: Self = 32767
}
extend Int16 with TryFrom<String, NumberParseError>
extend Int16 with TryFrom<Int64, NumberRangeError>
native type Int32 with Signed, Hash, Bits, Power<Int64>
extend Int32 {
  static zero: Self = 0
  static one: Self = 1
  static minimum: Self = -2147483648
  static maximum: Self = 2147483647
}
extend Int32 with TryFrom<String, NumberParseError>
extend Int32 with TryFrom<Int64, NumberRangeError>
native type Int64 with Signed, Hash, Bits, Power<Int64>
extend Int64 {
  static zero: Self = 0
  static one: Self = 1
  static minimum: Self = -9223372036854775807 - 1
  static maximum: Self = 9223372036854775807
  native static fn parseDigits(text: String, radix: Self): Result<Self, NumberParseError>
}
extend Int64 with From<Int8>
extend Int64 with From<Int16>
extend Int64 with From<Int32>
extend Int64 with From<UInt8>
extend Int64 with From<UInt16>
extend Int64 with From<UInt32>
extend Int64 with TryFrom<String, NumberParseError>
extend Int64 with TryFrom<UInt64, NumberRangeError>
extend Int64 with TryFrom<Float64, NumberRangeError>
extend Int64 with TryFrom<Decimal, NumberRangeError>
extend Int64 with From<Char>
extend Int64 {
  fn bytes(): ByteSize
  fn kilobytes(): ByteSize
  fn megabytes(): ByteSize
  fn gigabytes(): ByteSize
  fn terabytes(): ByteSize
  fn kibibytes(): ByteSize
  fn mebibytes(): ByteSize
  fn gibibytes(): ByteSize
  fn tebibytes(): ByteSize
}
native type UInt8 with Numeric, Hash, Bits, Power<Int64>
extend UInt8 {
  static zero: Self = 0
  static one: Self = 1
  static minimum: Self = 0
  static maximum: Self = 255
}
extend UInt8 with TryFrom<String, NumberParseError>
extend UInt8 with TryFrom<Int64, NumberRangeError>
native type UInt16 with Numeric, Hash, Bits, Power<Int64>
extend UInt16 {
  static zero: Self = 0
  static one: Self = 1
  static minimum: Self = 0
  static maximum: Self = 65535
}
extend UInt16 with TryFrom<String, NumberParseError>
extend UInt16 with TryFrom<Int64, NumberRangeError>
native type UInt32 with Numeric, Hash, Bits, Power<Int64>
extend UInt32 {
  static zero: Self = 0
  static one: Self = 1
  static minimum: Self = 0
  static maximum: Self = 4294967295
}
extend UInt32 with TryFrom<String, NumberParseError>
extend UInt32 with TryFrom<Int64, NumberRangeError>
native type UInt64 with Numeric, Hash, Bits, Power<Int64>
extend UInt64 {
  static zero: Self = 0
  static one: Self = 1
  static minimum: Self = 0
  static maximum: Self = ~Self.minimum
  native fn addedWrapping(other: Self): Self
  native fn multipliedWrapping(other: Self): Self
}
extend UInt64 with TryFrom<String, NumberParseError>
extend UInt64 with TryFrom<Int64, NumberRangeError>
extend UInt64 with TryFrom<Decimal, NumberRangeError>
native type Float32 with Signed
extend Float32 {
  static zero: Self = 0.0
  static one: Self = 1.0
  static nan: Self = 0.0 / 0.0
  static infinity: Self = 1.0 / 0.0
  static negativeInfinity: Self = -1.0 / 0.0
  fn isInfinite(): Bool
  fn isFinite(): Bool
  native fn bits(): UInt32
  native static fn ofBits(bits: UInt32): Self
}
extend Float32 with TryFrom<String, NumberParseError>
extend Float32 with TryFrom<Float64, NumberRangeError>
native type Float64 with Signed, Power
extend Float64 {
  static zero: Self = 0.0
  static one: Self = 1.0
  static pi: Self = 3.141592653589793
  static tau: Self = 6.283185307179586
  static epsilon: Self = 0.0000000000000002220446049250313
  static e: Self = 2.718281828459045
  static nan: Self = 0.0 / 0.0
  static infinity: Self = 1.0 / 0.0
  static negativeInfinity: Self = -1.0 / 0.0
  native fn squareRoot(): Self
  native fn floor(): Self
  native fn ceiling(): Self
  native fn round(): Self
  native fn isNaN(): Bool
  fn isInfinite(): Bool
  fn isFinite(): Bool
  native fn bits(): UInt64
  native static fn ofBits(bits: UInt64): Self
  fn isCloseTo(other: Self, tolerance: Self = 0.000001): Bool
}
extend Float64 with Real
extend Float64 with Power<Int64>
extend Float64 with From<Int64>
extend Float64 with From<Float32>
extend Float64 with From<Decimal>
extend Float64 with TryFrom<String, NumberParseError>
type Int = Int64
type UInt = UInt64
type Float = Float64

Every type carries its width in its name. Int8 through UInt64 are Hash and Integer, and each has a minimum and a maximum constant of its own type. Int64 is the type of an integer literal and additionally has parseDigits(text, radix:) for reading digits in another base (Int.parseDigits("ff", radix: 16), no sign, no prefix, _ allowed between digits) and addedWrapping/multipliedWrapping on UInt64, the only arithmetic in the language that does not panic on overflow - it exists for hash functions that need the wrap. Float64 is the type of a decimal literal and additionally has pi, e, squareRoot(), floor(), ceiling(), round(), isNaN() and isCloseTo(other, tolerance:), because == on a float is IEEE-754 (nan != nan, 0.0 == -0.0) while compare is a total order (nan is above everything, -0.0 compares equal to 0.0) - which is also why Float32 and Float64 are deliberately not Hash, so a float can never be a Map key and the nan key does not exist. Int, UInt and Float are plain aliases for the default width, nothing the language treats specially.

Both Float32 and Float64 also carry nan, infinity and negativeInfinity, and isInfinite() and isFinite() alongside isNaN() (Float64.nan, Float32.infinity.isInfinite()). nan and infinity are named the way pi and e are, but a constant expression that computes nan - 0.0 / 0.0 written anywhere else - stays a compile error at that expression (Floating-point numbers, rule 7): Float64.nan and Float32.nan are the only two declarations the constant evaluator lets fold to it, because that is what the name of each of them already says. An infinity has no such exception to make - it is an ordinary IEEE-754 value like any other, which is why 1.0 / 0.0 needs none to be a constant.

Every integer type is raised by an Int: 2 ** 10 is exact, a power that leaves the range of the type panics the way * does (arithmetic overflow in \**`), and so does a negative exponent, because its result is no whole number. A Float64is raised by aFloat64(C'spow) or by an Int(the samepow`, which rounds once where repeated squaring would round at every step), and never panics.

A Float32 is IEEE 754 binary32: every operator rounds its result to the nearest Float32, ties to even, so 0.1 + 0.2 of two Float32 is 0.3 where the Float64 sum is 0.30000000000000004. A literal or a text is read as a Float64 and rounded the same way, and "{value}" writes the shortest text that reads back to the same Float32. Float32.tryFrom of a Float64 rounds to the nearest Float32 and fails only for a finite value beyond its range.

bits() is the IEEE-754 pattern of a float as the unsigned integer of its width - a UInt64 for a Float64, a UInt32 for a Float32 - and ofBits(_) is the float of a pattern: 1.0.bits() is 0x3ff0000000000000, and a Float32 1.0 is 0x3f800000. Both are a memcpy and never a conversion, so every pattern makes the round trip with all its bits - -0.0, a subnormal, an infinity and a nan with a payload, a signalling one included - in the binary and in the VM alike. A call of either is never folded into a constant, because what ofBits makes may be a nan.

Float32 is not a Real yet: it has no squareRoot and no angle members. <math.h> has the float functions, and Real is one extend over them.

print(2 ** 10)
print(2.0 ** 0.5)
print Float.e.naturalLogarithm()
print((8.0).logarithm(base: 2.0))
// prints 1024
// prints 1.4142135623730951
// prints 1.0
// prints 3.0

There is no std/math#

std/math held pi, e, power, exponential, naturalLog, logarithm and the trigonometry as free functions of Float64. It is gone, because every one of them was a second way to write something the number already has: the trigonometry is Real's (angle.sine(), y.arcTangentDivided(by: x) for the old arcTangent2(y, x)), pi and e are Float.pi and Float.e, the power is the operator **, and exponential(), naturalLogarithm() and logarithm(base:) became members of Real. As members they are generic: a body over Scalar: Real computes with a Fixed exactly as with a Float, which free functions of Float64 never allowed.

Decimal#

native type Decimal with Signed, Hash
extend Decimal {
  static zero: Self = 0
  static one: Self = 1
}
extend Decimal {
  fn rounded(scale: Int32, rounding: Rounding): Self
  fn divided(by: Self, scale: Int32, rounding: Rounding): Self
  native fn scale(): Int32
  native fn normalized(): Self
  native fn unscaledText(): String
  static fn of(unscaled: String, scale: Int32): Result<Self, NumberParseError>
}
extend Decimal with From<Int64>
extend Decimal with From<UInt64>
extend Decimal with TryFrom<String, NumberParseError>

Exact base-10 arithmetic: a decimal literal adapts to Decimal the same way it adapts to Float64 (const price: Decimal = 19.99) and keeps every digit and its scale. See Decimal: both back ends compute it, and a value can also be read with Decimal.tryFrom or converted with Decimal.from.

ByteSize#

type ByteSize with Show, Compare, Add, Subtract, Multiply<Int64, ByteSize> {
  fn bytes(): Int64
  fn fraction(of: ByteSize): Float64
  fn kilobytes(): Float64
  fn megabytes(): Float64
  fn gigabytes(): Float64
  fn terabytes(): Float64
  fn kibibytes(): Float64
  fn mebibytes(): Float64
  fn gibibytes(): Float64
  fn tebibytes(): Float64
  fn compare(other: Self): Ordering
  fn add(other: Self): Self
  fn subtract(other: Self): Self
  fn multiply(other: Int64): Self
  fn show(): String
}

A number of bytes, the way a Duration is a number of nanoseconds: one Int64, signed, so the difference of two sizes may be negative. Int64 makes one in two families of units - kilobytes, megabytes, gigabytes and terabytes count in powers of 1000, as a disk is sold; kibibytes, mebibytes, gibibytes and tebibytes in powers of 1024, as a memory is counted - and the members of the same names read one back as a Float64. std/number owns Int64, so 64.megabytes() needs no import anywhere, and ByteSize itself is in the prelude.

const limit = 64.megabytes()
print limit.bytes()
print 1536.bytes()
print(2.gibibytes() - 512.mebibytes())
print 256.mebibytes().fraction(of: 1.gibibytes())
// prints 64000000
// prints 1.5 KiB
// prints 1.5 GiB
// prints 0.25

show writes the largest binary unit with at least one of it, to one decimal (15.9 GiB), and whole bytes below a kibibyte (512 B). So 64.megabytes() prints 61.0 MiB: the decimal readers are one call away when a number has to match what a disk vendor or a file manager says.

Conversions#

Widening between integer types, and from an integer or Float32 to Float64, is From and therefore infallible (Int64.from(byte) for a UInt8). Narrowing - Int64 down to Int8 or Int32, or Float64 to Int64 - is TryFrom<Source, NumberRangeError> instead, and so is Float64 to Float32, which rounds to the nearest Float32 and fails only beyond its range. Decimal.from takes an Int64 or a UInt64 exactly, Float64.from(decimal) rounds to the nearest Float64, and Int64.tryFrom and UInt64.tryFrom take a Decimal that is whole and in range. Int64.from(character) is the code point of a Char; the other direction is std/text's Char.tryFrom.

  • Integers - the widths, the defaults, and what overflow does.
  • Floating-point numbers - why == and compare disagree, and why floats are not Hash.
  • Decimal - exact base-ten arithmetic, and its digits through JSON, YAML and TOML.
  • std/core - Add, Subtract, Negate and the other operator traits Numeric and Signed build on.
  • Operators are traits - **, its precedence, and why ^ is no operator.
Esc