ExamplesBy LevelBy TopicLearning Paths
875 Intermediate

875-newtype-pattern — Newtype Pattern

Functional Programming

Tutorial

The Problem

Type aliases create convenient shorthand but provide no safety guarantees: type UserId = u64 means UserId and u64 are interchangeable. The newtype pattern wraps a type in a single-field struct, creating a distinct type with identical runtime representation. Haskell's newtype keyword formalizes this with zero-cost guarantee. In Rust, tuple structs like struct UserId(u64) achieve the same: you cannot accidentally pass a UserId where an OrderId is expected, even though both wrap u64. This pattern is standard in domain-driven design for preventing primitive obsession and encoding domain invariants.

🎯 Learning Outcomes

  • • Create newtypes using Rust tuple structs to prevent type confusion
  • • Implement validation-enforcing smart constructors that return Result
  • • Understand the difference between newtype (new type) and type alias (same type)
  • • Use Display and Debug implementations to give newtypes meaningful output
  • • Compare Rust tuple structs with OCaml's abstract module types for the same purpose
  • Code Example

    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    struct UserId(u64);
    
    impl UserId {
        fn new(id: u64) -> Option<Self> {
            if id > 0 { Some(UserId(id)) } else { None }
        }
        fn value(self) -> u64 { self.0 }
    }

    Key Differences

  • Zero-cost abstraction: Both Rust tuple structs and OCaml abstract types have zero runtime overhead compared to the wrapped type.
  • Syntax brevity: Rust struct Email(String) is three words; OCaml needs a full module definition.
  • Deriving: Rust can #[derive(Debug, Clone, Copy, PartialEq)]; OCaml requires explicit functor application or manual implementation.
  • Inner access: Rust's .0 field accessor breaks abstraction (unless private); OCaml abstract types are more strictly opaque without the module's value function.
  • OCaml Approach

    OCaml achieves the same effect through abstract module types. module UserId: sig type t; val create: int -> t; val value: t -> int end = struct type t = int; ... end makes UserId.t opaque outside the module. The inner int is inaccessible directly, and UserId.t and OrderId.t are incompatible types. OCaml's with type constraint can selectively expose the type alias when needed. The pattern is more verbose than Rust's tuple struct but achieves the same compile-time safety.

    Full Source

    #![allow(clippy::all)]
    // Example 081: Newtype Pattern
    // Rust tuple structs for type safety
    
    use std::fmt;
    
    // === Approach 1: Simple newtype wrappers ===
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    struct UserId(u64);
    
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    struct OrderId(u64);
    
    impl UserId {
        fn new(id: u64) -> Option<Self> {
            if id > 0 {
                Some(UserId(id))
            } else {
                None
            }
        }
        fn value(self) -> u64 {
            self.0
        }
    }
    
    impl OrderId {
        fn new(id: u64) -> Option<Self> {
            if id > 0 {
                Some(OrderId(id))
            } else {
                None
            }
        }
        fn value(self) -> u64 {
            self.0
        }
    }
    
    impl fmt::Display for UserId {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "User#{}", self.0)
        }
    }
    
    impl fmt::Display for OrderId {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "Order#{}", self.0)
        }
    }
    
    // Can't accidentally pass UserId where OrderId expected!
    fn process_order(order: OrderId, user: UserId) -> String {
        format!("{} placed {}", user, order)
    }
    
    // === Approach 2: Newtype with validation ===
    #[derive(Debug, Clone, PartialEq)]
    struct Email(String);
    
    impl Email {
        fn new(s: &str) -> Option<Self> {
            if s.contains('@') {
                Some(Email(s.to_string()))
            } else {
                None
            }
        }
        fn as_str(&self) -> &str {
            &self.0
        }
    }
    
    // === Approach 3: Newtype for unit safety ===
    #[derive(Debug, Clone, Copy, PartialEq)]
    struct Celsius(f64);
    
    #[derive(Debug, Clone, Copy, PartialEq)]
    struct Fahrenheit(f64);
    
    impl Celsius {
        fn new(v: f64) -> Self {
            Celsius(v)
        }
        fn to_fahrenheit(self) -> Fahrenheit {
            Fahrenheit(self.0 * 9.0 / 5.0 + 32.0)
        }
        fn value(self) -> f64 {
            self.0
        }
    }
    
    impl Fahrenheit {
        fn new(v: f64) -> Self {
            Fahrenheit(v)
        }
        fn to_celsius(self) -> Celsius {
            Celsius((self.0 - 32.0) * 5.0 / 9.0)
        }
        fn value(self) -> f64 {
            self.0
        }
    }
    
    // Implement Deref for transparent access when appropriate
    use std::ops::Deref;
    
    #[derive(Debug, Clone, PartialEq)]
    struct NonEmptyString(String);
    
    impl NonEmptyString {
        fn new(s: &str) -> Option<Self> {
            if s.is_empty() {
                None
            } else {
                Some(NonEmptyString(s.to_string()))
            }
        }
    }
    
    impl Deref for NonEmptyString {
        type Target = str;
        fn deref(&self) -> &str {
            &self.0
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_user_order_type_safety() {
            let u = UserId::new(1).unwrap();
            let o = OrderId::new(2).unwrap();
            assert_eq!(u.value(), 1);
            assert_eq!(o.value(), 2);
            // UserId and OrderId are different types
        }
    
        #[test]
        fn test_invalid_ids() {
            assert!(UserId::new(0).is_none());
            assert!(OrderId::new(0).is_none());
        }
    
        #[test]
        fn test_email_validation() {
            assert!(Email::new("a@b.com").is_some());
            assert!(Email::new("invalid").is_none());
        }
    
        #[test]
        fn test_temperature_conversion() {
            let c = Celsius::new(0.0);
            assert!((c.to_fahrenheit().value() - 32.0).abs() < 1e-10);
    
            let f = Fahrenheit::new(212.0);
            assert!((f.to_celsius().value() - 100.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_roundtrip() {
            let c = Celsius::new(37.0);
            let back = c.to_fahrenheit().to_celsius();
            assert!((back.value() - 37.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_non_empty_string() {
            assert!(NonEmptyString::new("hello").is_some());
            assert!(NonEmptyString::new("").is_none());
            let s = NonEmptyString::new("test").unwrap();
            assert_eq!(s.len(), 4); // Deref to &str
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_user_order_type_safety() {
            let u = UserId::new(1).unwrap();
            let o = OrderId::new(2).unwrap();
            assert_eq!(u.value(), 1);
            assert_eq!(o.value(), 2);
            // UserId and OrderId are different types
        }
    
        #[test]
        fn test_invalid_ids() {
            assert!(UserId::new(0).is_none());
            assert!(OrderId::new(0).is_none());
        }
    
        #[test]
        fn test_email_validation() {
            assert!(Email::new("a@b.com").is_some());
            assert!(Email::new("invalid").is_none());
        }
    
        #[test]
        fn test_temperature_conversion() {
            let c = Celsius::new(0.0);
            assert!((c.to_fahrenheit().value() - 32.0).abs() < 1e-10);
    
            let f = Fahrenheit::new(212.0);
            assert!((f.to_celsius().value() - 100.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_roundtrip() {
            let c = Celsius::new(37.0);
            let back = c.to_fahrenheit().to_celsius();
            assert!((back.value() - 37.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_non_empty_string() {
            assert!(NonEmptyString::new("hello").is_some());
            assert!(NonEmptyString::new("").is_none());
            let s = NonEmptyString::new("test").unwrap();
            assert_eq!(s.len(), 4); // Deref to &str
        }
    }

    Deep Comparison

    Comparison: Newtype Pattern

    Abstract Type vs Tuple Struct

    OCaml:

    module UserId : sig
      type t
      val create : int -> t
      val value : t -> int
    end = struct
      type t = int
      let create x = if x > 0 then x else failwith "invalid"
      let value x = x
    end
    

    Rust:

    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    struct UserId(u64);
    
    impl UserId {
        fn new(id: u64) -> Option<Self> {
            if id > 0 { Some(UserId(id)) } else { None }
        }
        fn value(self) -> u64 { self.0 }
    }
    

    Temperature Units

    OCaml:

    type celsius = { celsius_value : float }
    type fahrenheit = { fahrenheit_value : float }
    
    let to_fahrenheit c =
      { fahrenheit_value = c.celsius_value *. 9.0 /. 5.0 +. 32.0 }
    

    Rust:

    struct Celsius(f64);
    struct Fahrenheit(f64);
    
    impl Celsius {
        fn to_fahrenheit(self) -> Fahrenheit {
            Fahrenheit(self.0 * 9.0 / 5.0 + 32.0)
        }
    }
    

    Validated Data

    OCaml:

    module Email : sig
      type t = private string
      val create : string -> t option
    end = struct
      type t = string
      let create s = if String.contains s '@' then Some s else None
    end
    

    Rust:

    struct Email(String);
    
    impl Email {
        fn new(s: &str) -> Option<Self> {
            if s.contains('@') { Some(Email(s.to_string())) } else { None }
        }
    }
    

    Exercises

  • Implement struct Percentage(f64) with a smart constructor that validates the value is between 0.0 and 100.0.
  • Add arithmetic for Quantity<Unit> (a newtype-over-f64) that only allows same-unit addition but returns a plain f64 for division.
  • Implement struct NonNegative(i64) and derive Add, Sub (returning Result on underflow), and Mul for it.
  • Open Source Repos