Parameters and variables with logically distinct types must be statically distinguishable by the type system.
Use a newtype (e.g., struct Meters(u32);) when:
Two or more quantities share the same underlying primitive representation but are logically distinct
Confusing them would constitute a semantic error
You need to improve type safety and encapsulation
You need to enable trait-based behavior
You need to establish new invariants
|
|
|
|
This rule ensures that parameters and variables convey intent directly through the type system to avoid accidental misuse of values with identical primitives but different semantics.
In particular:
Prevents mixing logically distinct values.
Primitive types like u32 or u64 can represent lengths, counters, timestamps, durations, IDs, or other values.
Different semantic domains can be confused, leading to incorrect computations.
The Rust type system prevents such mistakes when semantics are encoded into distinct types.
Improves static safety.
Statically distinct types allow the compiler to enforce domain distinctions.
Accidental swapping of parameters or returning the wrong quantity becomes a compile-time error.
Improves readability and discoverability.
Intent-revealing names (Meters, Seconds, UserId) make code self-documenting.
Type signatures become easier to read and understand.
Enables domain-specific trait implementations.
Statically distinct types allow you to implement Add, Mul, or custom traits in ways that match the domain logic.
Aliases cannot do this, because they are not distinct types.
Supports API evolution.
Statically distinct types act as strong API contracts that can evolve independently from their underlying representations.
|
|
|
|
|
This noncompliant example uses primitive types directly, leading to potential confusion between distance and time.
Nothing prevents the caller from passing time as distance or vice-versa.
The units of each type are not clear from the function signature alone.
Mistakes compile cleanly and silently produce wrong results.
fn travel(distance: u32, time: u32) -> u32 {
distance / time
}
fn main() {
let d = 100;
let t = = 10;
let _result = travel(t, d); // Compiles, but semantically incorrect
}
|
|
|
|
|
This noncompliant example uses aliases instead of distinct types.
Aliases do not create new types, so the compiler cannot enforce distinctions between Meters and Seconds.
Aliases cannot do this, because they are not distinct types.
This noncompliant example uses primitive types directly, leading to potential confusion between distance and time.
Nothing prevents the caller from passing time as distance or vice-versa.
The units of each type are not clear from the function signature alone.
Mistakes compile cleanly and silently produce wrong results.
type Meters = u32;
type Seconds = u32;
type MetersPerSecond = u32;
fn travel(distance: Meters, time: Seconds) -> MetersPerSecond {
distance / time
}
fn main() {
let d: Meters = 100;
let t: Seconds = 10;
let _result = travel(t, d); // Compiles, but semantically incorrect
}
|
|
|
|
|
This compliant example uses newtypes to create distinct types for Meters, Seconds, and MetersPerSecond.
The compiler enforces correct usage, preventing accidental swapping of parameters.
The function signature clearly conveys the intended semantics of each parameter and return value.
use std::ops::Div;
#[derive(Debug, Clone, Copy)]
struct Meters(u32);
#[derive(Debug, Clone, Copy)]
struct Seconds(u32);
#[derive(Debug, Clone, Copy)]
struct MetersPerSecond(u32);
impl Div<Seconds> for Meters {
type Output = MetersPerSecond;
fn div(self, rhs: Seconds) -> Self::Output {
MetersPerSecond(self.0 / rhs.0)
}
}
fn main() {
let d = Meters(100);
let t = Seconds(10);
let result = d / t; // Clean and type-safe!
println!("{:?}", result); // MetersPerSecond(10)
}
|
|