ExamplesBy LevelBy TopicLearning Paths
389 Intermediate

389: Newtype Pattern

Functional Programming

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

  • • Understand how newtype structs provide type safety with zero runtime cost
  • • Learn how to implement Display, Add, and other traits selectively on newtypes
  • • See how validated constructors (Email::new) maintain invariants at construction time
  • • Understand the orphan rule benefit: newtypes enable implementing foreign traits for foreign types
  • • Learn when to use newtypes vs. type aliases (aliases are transparent; newtypes are opaque)
  • Code 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

  • Zero-cost: Rust's newtype Meters(f64) has identical layout to f64 — the compiler strips the wrapper; OCaml's abstract types may carry a tag word in boxed representations.
  • Selective impl: Rust lets you implement exactly the traits you want on a newtype (e.g., Add but not Mul); OCaml abstract types inherit no operations from the underlying type, forcing explicit re-exposure.
  • Pattern matching: Rust's newtype can be destructured with let Meters(x) = m; OCaml uses let Distance x = d with the same destructuring pattern.
  • Phantom types: Both languages support phantom types for unit checking; Rust uses 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());
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Unit system: Build a complete unit system with Meters, Feet, Seconds, and MetersPerSecond. Implement Div<Seconds> for Meters producing MetersPerSecond, preventing nonsensical unit combinations at compile time.
  • Validated types: Create 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>.
  • Newtype as foreign trait impl: You need 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.
  • Open Source Repos