Units and dimensions
Compile-time units for numbers
Status: proposal. Units of measure are not yet implemented. This page records the intended design and the questions that remain open; see Open questions at the end.
Nanyx will support units of measure for numeric types, allowing you to express time, sizes, and rates safely.
def lookback = 30d
def timeout = 3000ms
def maxFilesize = 50MB
This page defines the design direction for units, with two goals:
- Unit-safe arithmetic with automatic unit derivation (for example
m / s -> m/s) - Ergonomic value construction at call sites (for example
delay(500ms))
Two concepts: dimensions and units
Nanyx should model dimensions and units separately:
- Dimension: the physical kind used by type checking (
Time,Length,Mass,Length/Time) - Unit: a named scale for a dimension (
ms,s,min,km)
Type checking operates on dimensions. Units are syntax and conversion metadata for constructing and displaying values.
Defining units
Units are always numeric in Nanyx. Unit declarations do not specify an underlying primitive type.
dimension Time
dimension Bytes
dimension Length
@baseUnit unit(m) Metres: Length
@baseUnit unit(s) Seconds: Time
unit(ms) Milliseconds: Time = 0.001s
unit(min) Minutes: Time = 60s
unit(hr) Hours: Time = 60min
unit(d) Days: Time = 24hr
@baseUnit unit(B) Byte: Bytes
unit(KB) Kilobyte: Bytes = 1024B
unit(MB) Megabyte: Bytes = 1024KB
Base vs derived units
A dimension defines at least one base unit.
- A unit marked with
@baseUnitis a base unit. - A unit declared with
=is a derived unit. =never defines a base unit; it always means “defined in terms of other units”.
Every derived unit must resolve through a finite chain to a base unit of the same dimension.
dimension Time
@baseUnit unit(s) Seconds: Time
unit(ms) Milliseconds: Time = 0.001s
unit(min) Minutes: Time = 60s
Whether a dimension may have more than one base unit (for example both s and tick for Time) is unresolved; see Open questions.
Cycles are invalid and must be rejected by the compiler:
unit(a) A: Time = 2b
unit(b) B: Time = 3a
-- error: cyclic derived-unit definition (A -> B -> A)
Design model
Each measured numeric type carries a compile-time dimension vector.
This page writes a measured type as number(Dimension). Once the Num(a) numeric context lands, the dimension is expected to become a parameter on the concrete numeric type instead, for example float(Time) or int(Bytes); the rules below are unaffected by that spelling.
numberis dimensionless (1)- Base dimensions are independent (for example
Length,Time,Mass) - Units map to a scale within one dimension
Conceptually:
number(Length)means a quantity of lengthnumber(Time)means a quantity of timenumber(Length/Time)is speednumber(Mass*Length/Time^2)is force
The compiler stores normalized dimensions (sorted bases + integer exponents), so equivalent forms unify:
Length/Time==Length*Time^-1(Length/Time)*Time==Length
Derived units
def transferRate = 50(MB / s)
Type rules (F#-style behavior)
Dimensions participate in type inference and unification, not runtime values.
For arithmetic operators on measured numeric values:
+and-require identical dimensions*adds exponents (d1 * d2)/subtracts exponents (d1 / d2)pow(x, n)multiplies exponents byn(for integern)
Examples:
def d = 100m
def t = 9.58s
def v = d / t -- inferred: number(Length/Time)
def a = v / t -- inferred: number(Length/Time^2)
Addition mismatch is rejected:
def bad = 10m + 2s
-- error: cannot add number(Length) and number(Time)
Generic constraints over dimensions
Functions can stay generic over dimensions:
def square: number(d) -> number(d^2) = { x -> x * x }
def avgSpeed: (number(Length), number(Time)) -> number(Length/Time) = { dist, time -> dist / time }
This keeps APIs reusable while preserving dimension correctness.
Conversion rules
Unit declarations define scale factors between units of the same dimension.
@baseUnit unit(s) Seconds: Time
unit(ms) Milliseconds: Time = 0.001s
unit(min) Minutes: Time = 60s
Conversions are compile-time checked and only allowed within equivalent dimensions:
def timeout: number(Time) = 500ms
def twoMinutes = 120s as min -- explicit conversion syntax (proposed)
Invalid conversion is rejected:
def impossible = 1m as s
Ergonomics for delay(500ms)
Use unit suffix literals as typed numeric constructors.
500msparses as numeric literal500with unit suffixms- Type is inferred as
number(Time)(carrying source unit metadata for conversions/formatting) - Works anywhere a numeric expression works
type Duration = number(Time)
def delay: Duration -> () = { d -> ... }
delay(500ms) -- ok
delay(0.5s) -- ok
delay(2min) -- ok
delay(42) -- error: expected Duration, found dimensionless number
Function parameters: dimensions vs units
Both are useful, but they should mean different things.
- Default: functions accept a dimension (
number(Time),number(Length/Time), etc.) - Optional strict mode: functions can request an exact unit when protocol/interop requires it (for example milliseconds only)
Dimension-first API (recommended):
def delay: number(Time) -> () = { d -> ... }
delay(500ms)
delay(0.5s)
delay(2min)
Unit-specific API (proposed constraint form):
def setTimeoutRaw: number(Time) where unit = ms -> () = { d -> ... }
setTimeoutRaw(500ms) -- ok
setTimeoutRaw(1s) -- error (or require explicit `as ms`)
Recommendation: keep most APIs dimension-based, and reserve unit-specific parameters for boundaries (wire formats, hardware APIs, legacy contracts).
Why this satisfies both goals
- Goal 1: unit algebra happens in inference/unification, so results derive automatically
- Goal 2: suffix literals construct measured values directly at call sites
Runtime model
Dimensions and units are erased after type checking.
- Runtime carries only the underlying numeric representation
- No runtime penalty for unit checking
- Optional debug metadata can be emitted by tooling, not required by execution
Open questions
The following need answers before this design can be implemented. They are listed in the order in which their answers constrain the rest.
- Where the scale lives. If the unit is not part of the type, then
500ms + 1smust be computed in a canonical unit, which means every literal is converted to a per-dimension base at compile time and the “source unit metadata” mentioned above is for display only. That in turn requires exactly one base unit per dimension, or a declared scale between any additional base units. The current text allows several base units with no relation between them, which cannot work. - Integers.
0.001sand1024Bmean conversions are not closed overint. If the canonical unit forTimeiss, then500msis0.5and everyTimeis a float. For integer-flavoured dimensions such asBytesthe base unit probably needs to be the smallest unit, with larger units as integer multiples. - Interaction with
Num(a). Withnumberbecoming a numeric context rather than a type, a measured value needs somewhere to carry its dimension. The likely answer is a dimension parameter on the concrete numeric type (float(Time),int(Bytes)), which also gives a per-dimension answer to question 2. - Type-level exponent arithmetic.
number(d^2)andLength/Time^2need integer exponents in the type language and unification that can solve equations such asd * d = Length^2. This is the largest implementation piece; Kennedy’s “Types for Units-of-Measure” describes an algorithm that fits an HM-style checker. - Lexing of suffix literals.
100mmust be a single literal while100 mis not,mmust not collide with an identifier in scope (such as an import alias), and the interaction with exponent notation (1e5) and underscores (1_000ms) needs stating.