389: Newtype Pattern
Tutorial Video
Text description (accessibility)
This video demonstrates the "389: Newtype Pattern" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Primitive types like `f64` and `String` carry no semantic meaning. Key difference from OCaml: 1. **Zero
Tutorial
The Problem
Primitive types like f64 and String carry no semantic meaning. A function taking two f64 parameters for distance and mass has no protection against passing them in the wrong order. The Mars Climate Orbiter was lost in 1999 because one module used metric units and another used imperial — the compiler saw only f64. The newtype pattern wraps primitive types in single-field structs, creating distinct types with zero runtime overhead: Meters(f64) and Kilograms(f64) cannot be accidentally interchanged.
Newtypes also enable implementing foreign traits on foreign types (since the newtype is yours), adding validation in constructors, and creating refined types like Email(String) that guarantee invariants.
🎯 Learning Outcomes
Display, Add, and other traits selectively on newtypesEmail::new) maintain invariants at construction timeCode Example
#![allow(clippy::all)]
//! Newtype Pattern
use std::fmt;
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Meters(pub f64);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Kilograms(pub f64);
impl fmt::Display for Meters {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2}m", self.0)
}
}
impl fmt::Display for Kilograms {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2}kg", self.0)
}
}
impl Add for Meters {
type Output = Meters;
fn add(self, o: Meters) -> Meters {
Meters(self.0 + o.0)
}
}
pub struct Email(String);
impl Email {
pub fn new(s: &str) -> Option<Self> {
if s.contains('@') && s.contains('.') {
Some(Email(s.to_string()))
} else {
None
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_meters_display() {
assert_eq!(format!("{}", Meters(5.0)), "5.00m");
}
#[test]
fn test_meters_add() {
assert_eq!(Meters(2.0) + Meters(3.0), Meters(5.0));
}
#[test]
fn test_email_valid() {
assert!(Email::new("a@b.com").is_some());
}
#[test]
fn test_email_invalid() {
assert!(Email::new("invalid").is_none());
}
}Key Differences
Meters(f64) has identical layout to f64 — the compiler strips the wrapper; OCaml's abstract types may carry a tag word in boxed representations.Add but not Mul); OCaml abstract types inherit no operations from the underlying type, forcing explicit re-exposure.let Meters(x) = m; OCaml uses let Distance x = d with the same destructuring pattern.PhantomData<T>, OCaml uses a phantom type parameter 'a.OCaml Approach
OCaml achieves the same with private types in modules: module Meters : sig type t val create : float -> t val value : t -> float end. The .mli file hides the constructor so users must go through Meters.create. Abstract types in OCaml are the direct equivalent of Rust's opaque newtypes. For unit checking, OCaml can also use phantom types: type 'a distance = Distance of float where 'a is meters or feet.
Full Source
#![allow(clippy::all)]
//! Newtype Pattern
use std::fmt;
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Meters(pub f64);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Kilograms(pub f64);
impl fmt::Display for Meters {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2}m", self.0)
}
}
impl fmt::Display for Kilograms {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2}kg", self.0)
}
}
impl Add for Meters {
type Output = Meters;
fn add(self, o: Meters) -> Meters {
Meters(self.0 + o.0)
}
}
pub struct Email(String);
impl Email {
pub fn new(s: &str) -> Option<Self> {
if s.contains('@') && s.contains('.') {
Some(Email(s.to_string()))
} else {
None
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_meters_display() {
assert_eq!(format!("{}", Meters(5.0)), "5.00m");
}
#[test]
fn test_meters_add() {
assert_eq!(Meters(2.0) + Meters(3.0), Meters(5.0));
}
#[test]
fn test_email_valid() {
assert!(Email::new("a@b.com").is_some());
}
#[test]
fn test_email_invalid() {
assert!(Email::new("invalid").is_none());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_meters_display() {
assert_eq!(format!("{}", Meters(5.0)), "5.00m");
}
#[test]
fn test_meters_add() {
assert_eq!(Meters(2.0) + Meters(3.0), Meters(5.0));
}
#[test]
fn test_email_valid() {
assert!(Email::new("a@b.com").is_some());
}
#[test]
fn test_email_invalid() {
assert!(Email::new("invalid").is_none());
}
}
Deep Comparison
OCaml vs Rust: 389-newtype-pattern
Exercises
Meters, Feet, Seconds, and MetersPerSecond. Implement Div<Seconds> for Meters producing MetersPerSecond, preventing nonsensical unit combinations at compile time.Username(String), Password(String), and Age(u8) newtypes with validated constructors that enforce: username 3-20 chars alphanumeric, password minimum 8 chars, age 0-150. Return Result<Self, String>.Display for Vec<i32> (a foreign trait on a foreign type). Create DisplayVec(Vec<i32>) newtype and implement Display for it, then implement From<Vec<i32>> for ergonomic conversion.