081 — Newtype Pattern
Tutorial
The Problem
Use single-field tuple structs (struct Meters(f64)) to give distinct types to values that share the same underlying representation. Prevent units-of-measure confusion (Meters vs Seconds), enforce distinct ID types (UserId vs OrderId), and add type-safe conversions between Celsius and Fahrenheit — all with zero runtime overhead.
🎯 Learning Outcomes
From<Celsius> for Fahrenheit for ergonomic .into() conversionstype meters = Meters of float)Code Example
#![allow(clippy::all)]
// 081: Newtype Pattern
// Wrapping primitives for type safety at zero cost
// Approach 1: Simple newtypes
#[derive(Debug, Clone, Copy)]
struct Meters(f64);
#[derive(Debug, Clone, Copy)]
struct Seconds(f64);
#[derive(Debug, Clone, Copy)]
struct MetersPerSecond(f64);
fn speed(distance: Meters, time: Seconds) -> Option<MetersPerSecond> {
if time.0 == 0.0 {
None
} else {
Some(MetersPerSecond(distance.0 / time.0))
}
}
// Approach 2: Distinct ID types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct OrderId(u64);
// These are different types — can't mix them!
fn find_user(id: UserId) -> String {
format!("User #{}", id.0)
}
fn find_order(id: OrderId) -> String {
format!("Order #{}", id.0)
}
// Approach 3: Newtype with conversions
#[derive(Debug, Clone, Copy)]
struct Celsius(f64);
#[derive(Debug, Clone, Copy)]
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
impl From<Fahrenheit> for Celsius {
fn from(f: Fahrenheit) -> Self {
Celsius((f.0 - 32.0) * 5.0 / 9.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_speed() {
let s = speed(Meters(100.0), Seconds(10.0)).unwrap();
assert!((s.0 - 10.0).abs() < 0.001);
assert!(speed(Meters(100.0), Seconds(0.0)).is_none());
}
#[test]
fn test_distinct_ids() {
assert_eq!(find_user(UserId(42)), "User #42");
assert_eq!(find_order(OrderId(7)), "Order #7");
// UserId(1) != OrderId(1) — different types, won't even compile if compared
}
#[test]
fn test_temperature() {
let f: Fahrenheit = Celsius(100.0).into();
assert!((f.0 - 212.0).abs() < 0.001);
let c: Celsius = Fahrenheit(32.0).into();
assert!(c.0.abs() < 0.001);
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Syntax | struct Meters(f64) | type meters = Meters of float |
| Runtime cost | Zero (transparent layout) | Possible boxing for float in records |
| Unwrapping | .0 field access | Pattern match (Meters m) |
| Conversion | impl From<A> for B | Named function to_celsius |
| Ergonomics | .into() / From::from | Explicit call |
| Compile-time safety | Full (different types) | Full (different constructors) |
Both languages provide the same semantic guarantee: you cannot pass a Meters value where a Seconds is expected. Rust adds the From/Into trait infrastructure for ergonomic conversions; OCaml relies on explicit function naming.
OCaml Approach
OCaml achieves the same safety with single-constructor variants: type meters = Meters of float. Pattern matching in function arguments (let speed (Meters d) (Seconds t)) destructures automatically. OCaml variants are slightly heavier than Rust newtypes in that they may not be unboxed in all contexts, but the safety guarantee is equivalent. The to_fahrenheit and to_celsius functions are explicit conversions — OCaml has no From/Into trait system, so conversions require naming.
Full Source
#![allow(clippy::all)]
// 081: Newtype Pattern
// Wrapping primitives for type safety at zero cost
// Approach 1: Simple newtypes
#[derive(Debug, Clone, Copy)]
struct Meters(f64);
#[derive(Debug, Clone, Copy)]
struct Seconds(f64);
#[derive(Debug, Clone, Copy)]
struct MetersPerSecond(f64);
fn speed(distance: Meters, time: Seconds) -> Option<MetersPerSecond> {
if time.0 == 0.0 {
None
} else {
Some(MetersPerSecond(distance.0 / time.0))
}
}
// Approach 2: Distinct ID types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct OrderId(u64);
// These are different types — can't mix them!
fn find_user(id: UserId) -> String {
format!("User #{}", id.0)
}
fn find_order(id: OrderId) -> String {
format!("Order #{}", id.0)
}
// Approach 3: Newtype with conversions
#[derive(Debug, Clone, Copy)]
struct Celsius(f64);
#[derive(Debug, Clone, Copy)]
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
impl From<Fahrenheit> for Celsius {
fn from(f: Fahrenheit) -> Self {
Celsius((f.0 - 32.0) * 5.0 / 9.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_speed() {
let s = speed(Meters(100.0), Seconds(10.0)).unwrap();
assert!((s.0 - 10.0).abs() < 0.001);
assert!(speed(Meters(100.0), Seconds(0.0)).is_none());
}
#[test]
fn test_distinct_ids() {
assert_eq!(find_user(UserId(42)), "User #42");
assert_eq!(find_order(OrderId(7)), "Order #7");
// UserId(1) != OrderId(1) — different types, won't even compile if compared
}
#[test]
fn test_temperature() {
let f: Fahrenheit = Celsius(100.0).into();
assert!((f.0 - 212.0).abs() < 0.001);
let c: Celsius = Fahrenheit(32.0).into();
assert!(c.0.abs() < 0.001);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_speed() {
let s = speed(Meters(100.0), Seconds(10.0)).unwrap();
assert!((s.0 - 10.0).abs() < 0.001);
assert!(speed(Meters(100.0), Seconds(0.0)).is_none());
}
#[test]
fn test_distinct_ids() {
assert_eq!(find_user(UserId(42)), "User #42");
assert_eq!(find_order(OrderId(7)), "Order #7");
// UserId(1) != OrderId(1) — different types, won't even compile if compared
}
#[test]
fn test_temperature() {
let f: Fahrenheit = Celsius(100.0).into();
assert!((f.0 - 212.0).abs() < 0.001);
let c: Celsius = Fahrenheit(32.0).into();
assert!(c.0.abs() < 0.001);
}
}
Deep Comparison
Core Insight
A newtype wraps a primitive to create a distinct type. Meters(5.0) and Feet(5.0) are different types — the compiler prevents accidental mixing. Zero runtime overhead in both languages.
OCaml Approach
type meters = Meters of float (single-variant)Rust Approach
struct Meters(f64)Comparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Syntax | type t = T of inner | struct T(inner); |
| Access | Pattern match | .0 field access |
| Overhead | Zero | Zero |
| Trait impl | N/A | Can impl traits |
Exercises
Kilometers(f64) newtype and implement From<Kilometers> for Meters and vice versa.NonEmptyString(String) newtype with a constructor fn new(s: String) -> Option<NonEmptyString> that returns None for empty strings.std::ops::Add<Meters> for Meters so that two distances can be summed while still being Meters.std::fmt::Display for Celsius to print values as "100°C" and Fahrenheit as "212°F".validated_email newtype and a smart constructor val make : string -> validated_email option. Compare the pattern with Rust's equivalent approach.