ExamplesBy LevelBy TopicLearning Paths
135 Intermediate

Generic Newtype Patterns

Functional Programming

Tutorial

The Problem

Primitive types like String or u32 carry no domain meaning — a user ID and a product ID are both u32, but passing one where the other is expected is a bug the type checker won't catch. Newtypes wrap primitives in named types that are distinct at the type level, add invariants via smart constructors, and enable implementing external traits on otherwise opaque types. This is one of the most practical patterns in production Rust code.

🎯 Learning Outcomes

  • • Understand the newtype pattern: a single-field tuple struct wrapping another type
  • • Learn validated newtypes that enforce invariants at construction time
  • • See the generic Validated<T, V> pattern parameterized by a validator trait
  • • Practice transparent newtypes via Deref to expose the inner API without boilerplate
  • Code Example

    #[derive(Debug, Clone, PartialEq)]
    pub struct Email(String);
    
    impl Email {
        pub fn new(s: &str) -> Result<Self, &'static str> {
            if s.contains('@') { Ok(Email(s.to_owned())) }
            else { Err("invalid email: missing '@'") }
        }
        pub fn as_str(&self) -> &str { &self.0 }
    }
    
    // Typed IDs — same underlying u64, completely distinct types
    pub struct UserId(pub u64);
    pub struct ProductId(pub u64);

    Key Differences

  • Abstraction boundary: OCaml's module system is the mechanism for newtype abstraction (opaque module types); Rust uses private fields in tuple structs.
  • Deref transparency: Rust's Deref lets newtypes participate in auto-dereferencing for ergonomic access; OCaml has no equivalent — wrapper modules require explicit delegation.
  • Trait orphan rules: Rust newtypes can implement external traits (e.g., Display for Vec<T>); OCaml modules can implement signatures freely with no orphan restrictions.
  • Generic validators: Rust's Validated<T, V: Validator> is parameterized at the type level; OCaml achieves this via parameterized module functors.
  • OCaml Approach

    OCaml uses modules for validated newtypes:

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

    The module signature hides the concrete type, preventing direct construction outside the module. This achieves the same invariant-enforcement as Rust's private tuple fields, but at the module granularity rather than the type granularity.

    Full Source

    #![allow(clippy::all)]
    //! Example 135: Generic Newtype Patterns
    //!
    //! Wrap primitives and collections in named types to prevent mix-ups, add
    //! invariants, and give behaviour to types you don't own.
    //!
    //! # Approaches
    //!
    //! 1. **Validated newtypes** — construction validates an invariant; the type
    //!    proves validity to every caller.
    //! 2. **Generic validated wrapper** — a single `Validated<T, V>` struct
    //!    parameterised by a *validator* trait, mirroring OCaml's functor pattern.
    //! 3. **Transparent newtypes via `Deref`** — expose the inner API without
    //!    boilerplate while still keeping the distinct type.
    
    use std::fmt;
    use std::marker::PhantomData;
    use std::ops::Deref;
    
    // ── Approach 1: Validated domain newtypes ─────────────────────────────────────
    
    /// A validated e-mail address.  Can only be constructed through `Email::new`.
    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
    pub struct Email(String);
    
    impl Email {
        /// Returns `Ok(Email)` when `s` contains `'@'`, otherwise `Err`.
        pub fn new(s: &str) -> Result<Self, &'static str> {
            if s.contains('@') {
                Ok(Email(s.to_owned()))
            } else {
                Err("invalid email: missing '@'")
            }
        }
    
        /// Borrow the inner string slice.
        pub fn as_str(&self) -> &str {
            &self.0
        }
    }
    
    impl fmt::Display for Email {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            self.0.fmt(f)
        }
    }
    
    /// A validated username (>= 3 characters).
    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
    pub struct Username(String);
    
    impl Username {
        /// Returns `Ok(Username)` when `s` has at least 3 characters, otherwise `Err`.
        pub fn new(s: &str) -> Result<Self, &'static str> {
            if s.len() >= 3 {
                Ok(Username(s.to_owned()))
            } else {
                Err("username too short (< 3 chars)")
            }
        }
    }
    
    /// Transparent access to `str` methods without boilerplate forwarding.
    impl Deref for Username {
        type Target = str;
        fn deref(&self) -> &str {
            &self.0
        }
    }
    
    impl fmt::Display for Username {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            self.0.fmt(f)
        }
    }
    
    // Typed IDs -- same underlying type, distinct at compile time.
    
    /// A user's unique identifier.
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
    pub struct UserId(pub u64);
    
    /// A product's unique identifier.
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
    pub struct ProductId(pub u64);
    
    /// Accept only a `UserId`, never a raw `u64` or a `ProductId`.
    pub fn find_user(id: UserId) -> String {
        format!("user:{}", id.0)
    }
    
    // ── Approach 2: Generic validated wrapper (OCaml functor parallel) ────────────
    
    /// A validation strategy.  Implement this for a zero-sized marker type.
    pub trait Validator<T> {
        type Error: fmt::Debug + fmt::Display;
        fn validate(value: &T) -> Result<(), Self::Error>;
    }
    
    /// A value that has been checked by `V`.
    ///
    /// Construction is the only path to a `Validated<T, V>` -- no public field.
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub struct Validated<T, V>(T, PhantomData<V>);
    
    impl<T, V: Validator<T>> Validated<T, V> {
        /// Run the validator; return the wrapped value on success.
        pub fn new(value: T) -> Result<Self, V::Error> {
            V::validate(&value)?;
            Ok(Validated(value, PhantomData))
        }
    
        /// Borrow the inner value.
        pub fn inner(&self) -> &T {
            &self.0
        }
    
        /// Consume and return the inner value.
        pub fn into_inner(self) -> T {
            self.0
        }
    }
    
    impl<T: fmt::Display, V> fmt::Display for Validated<T, V> {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            self.0.fmt(f)
        }
    }
    
    /// Validator: integer must be positive.
    pub struct Positive;
    
    impl Validator<i64> for Positive {
        type Error = String;
        fn validate(value: &i64) -> Result<(), String> {
            if *value > 0 {
                Ok(())
            } else {
                Err(format!("{value} is not positive"))
            }
        }
    }
    
    /// Validator: string must be non-empty.
    pub struct NonEmpty;
    
    impl Validator<String> for NonEmpty {
        type Error = &'static str;
        fn validate(value: &String) -> Result<(), &'static str> {
            if value.is_empty() {
                Err("value must not be empty")
            } else {
                Ok(())
            }
        }
    }
    
    /// A positive integer, guaranteed by the type.
    pub type PositiveInt = Validated<i64, Positive>;
    
    /// A non-empty string, guaranteed by the type.
    pub type NonEmptyStr = Validated<String, NonEmpty>;
    
    // ── Approach 3: Newtype over a collection ─────────────────────────────────────
    
    /// An ordered list of scores.  Exposes read-only slice access via `Deref` but
    /// controls mutation so invariants (e.g. sorted order) can be enforced.
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub struct ScoreList(Vec<u32>);
    
    impl ScoreList {
        pub fn new(scores: Vec<u32>) -> Self {
            ScoreList(scores)
        }
    
        /// Push a score and keep the list sorted.
        pub fn insert_sorted(&mut self, score: u32) {
            let pos = self.0.partition_point(|&s| s <= score);
            self.0.insert(pos, score);
        }
    
        /// Arithmetic mean, or `None` when the list is empty.
        pub fn mean(&self) -> Option<f64> {
            if self.0.is_empty() {
                None
            } else {
                Some(self.0.iter().map(|&s| s as f64).sum::<f64>() / self.0.len() as f64)
            }
        }
    }
    
    impl Deref for ScoreList {
        type Target = [u32];
        fn deref(&self) -> &[u32] {
            &self.0
        }
    }
    
    // ─────────────────────────────────────────────────────────────────────────────
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // -- Email -----------------------------------------------------------------
    
        #[test]
        fn email_valid_accepted() {
            let e = Email::new("alice@example.com").unwrap();
            assert_eq!(e.as_str(), "alice@example.com");
        }
    
        #[test]
        fn email_missing_at_rejected() {
            assert!(Email::new("notanemail").is_err());
        }
    
        #[test]
        fn email_display_shows_address() {
            let e = Email::new("x@y.com").unwrap();
            assert_eq!(e.to_string(), "x@y.com");
        }
    
        // -- Username --------------------------------------------------------------
    
        #[test]
        fn username_valid_accepted() {
            let u = Username::new("bob").unwrap();
            // Deref lets us call str methods directly.
            assert_eq!(u.to_uppercase(), "BOB");
        }
    
        #[test]
        fn username_too_short_rejected() {
            assert!(Username::new("ab").is_err());
            assert!(Username::new("").is_err());
        }
    
        // -- Typed IDs -- no accidental swap ---------------------------------------
    
        #[test]
        fn typed_ids_are_distinct_in_find_user() {
            let uid = UserId(42);
            // find_user(ProductId(42)) would be a compile error -- types differ.
            assert_eq!(find_user(uid), "user:42");
        }
    
        #[test]
        fn typed_ids_ordering() {
            assert!(UserId(1) < UserId(2));
            assert!(ProductId(10) > ProductId(5));
        }
    
        // -- Generic Validated wrapper ---------------------------------------------
    
        #[test]
        fn positive_int_accepts_positive() {
            let n = PositiveInt::new(7).unwrap();
            assert_eq!(*n.inner(), 7);
        }
    
        #[test]
        fn positive_int_rejects_zero_and_negative() {
            assert!(PositiveInt::new(0).is_err());
            assert!(PositiveInt::new(-3).is_err());
        }
    
        #[test]
        fn positive_int_into_inner() {
            let n = PositiveInt::new(99).unwrap();
            assert_eq!(n.into_inner(), 99_i64);
        }
    
        #[test]
        fn non_empty_str_accepts_content() {
            let s = NonEmptyStr::new("hello".to_owned()).unwrap();
            assert_eq!(s.inner(), "hello");
        }
    
        #[test]
        fn non_empty_str_rejects_empty() {
            assert!(NonEmptyStr::new(String::new()).is_err());
        }
    
        // -- ScoreList -------------------------------------------------------------
    
        #[test]
        fn score_list_mean_empty_is_none() {
            let sl = ScoreList::new(vec![]);
            assert_eq!(sl.mean(), None);
        }
    
        #[test]
        fn score_list_mean_computed_correctly() {
            let sl = ScoreList::new(vec![10, 20, 30]);
            assert!((sl.mean().unwrap() - 20.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn score_list_insert_sorted_maintains_order() {
            let mut sl = ScoreList::new(vec![10, 30, 50]);
            sl.insert_sorted(25);
            sl.insert_sorted(5);
            assert_eq!(&*sl, &[5, 10, 25, 30, 50]);
        }
    
        #[test]
        fn score_list_deref_gives_slice_access() {
            let sl = ScoreList::new(vec![1, 2, 3]);
            // `.len()` and `.iter()` come from `Deref<Target = [u32]>`.
            assert_eq!(sl.len(), 3);
            let doubled: Vec<u32> = sl.iter().map(|&s| s * 2).collect();
            assert_eq!(doubled, [2, 4, 6]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // -- Email -----------------------------------------------------------------
    
        #[test]
        fn email_valid_accepted() {
            let e = Email::new("alice@example.com").unwrap();
            assert_eq!(e.as_str(), "alice@example.com");
        }
    
        #[test]
        fn email_missing_at_rejected() {
            assert!(Email::new("notanemail").is_err());
        }
    
        #[test]
        fn email_display_shows_address() {
            let e = Email::new("x@y.com").unwrap();
            assert_eq!(e.to_string(), "x@y.com");
        }
    
        // -- Username --------------------------------------------------------------
    
        #[test]
        fn username_valid_accepted() {
            let u = Username::new("bob").unwrap();
            // Deref lets us call str methods directly.
            assert_eq!(u.to_uppercase(), "BOB");
        }
    
        #[test]
        fn username_too_short_rejected() {
            assert!(Username::new("ab").is_err());
            assert!(Username::new("").is_err());
        }
    
        // -- Typed IDs -- no accidental swap ---------------------------------------
    
        #[test]
        fn typed_ids_are_distinct_in_find_user() {
            let uid = UserId(42);
            // find_user(ProductId(42)) would be a compile error -- types differ.
            assert_eq!(find_user(uid), "user:42");
        }
    
        #[test]
        fn typed_ids_ordering() {
            assert!(UserId(1) < UserId(2));
            assert!(ProductId(10) > ProductId(5));
        }
    
        // -- Generic Validated wrapper ---------------------------------------------
    
        #[test]
        fn positive_int_accepts_positive() {
            let n = PositiveInt::new(7).unwrap();
            assert_eq!(*n.inner(), 7);
        }
    
        #[test]
        fn positive_int_rejects_zero_and_negative() {
            assert!(PositiveInt::new(0).is_err());
            assert!(PositiveInt::new(-3).is_err());
        }
    
        #[test]
        fn positive_int_into_inner() {
            let n = PositiveInt::new(99).unwrap();
            assert_eq!(n.into_inner(), 99_i64);
        }
    
        #[test]
        fn non_empty_str_accepts_content() {
            let s = NonEmptyStr::new("hello".to_owned()).unwrap();
            assert_eq!(s.inner(), "hello");
        }
    
        #[test]
        fn non_empty_str_rejects_empty() {
            assert!(NonEmptyStr::new(String::new()).is_err());
        }
    
        // -- ScoreList -------------------------------------------------------------
    
        #[test]
        fn score_list_mean_empty_is_none() {
            let sl = ScoreList::new(vec![]);
            assert_eq!(sl.mean(), None);
        }
    
        #[test]
        fn score_list_mean_computed_correctly() {
            let sl = ScoreList::new(vec![10, 20, 30]);
            assert!((sl.mean().unwrap() - 20.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn score_list_insert_sorted_maintains_order() {
            let mut sl = ScoreList::new(vec![10, 30, 50]);
            sl.insert_sorted(25);
            sl.insert_sorted(5);
            assert_eq!(&*sl, &[5, 10, 25, 30, 50]);
        }
    
        #[test]
        fn score_list_deref_gives_slice_access() {
            let sl = ScoreList::new(vec![1, 2, 3]);
            // `.len()` and `.iter()` come from `Deref<Target = [u32]>`.
            assert_eq!(sl.len(), 3);
            let doubled: Vec<u32> = sl.iter().map(|&s| s * 2).collect();
            assert_eq!(doubled, [2, 4, 6]);
        }
    }

    Deep Comparison

    OCaml vs Rust: Generic Newtype Patterns

    Side-by-Side Code

    OCaml

    (* Algebraic variant as newtype *)
    type email = Email of string
    
    let email_of_string s =
      if String.contains s '@' then Some (Email s) else None
    
    let string_of_email (Email s) = s
    
    (* Functor-based generic wrapper *)
    module type VALIDATOR = sig
      type t
      val validate : t -> bool
    end
    
    module Validated (V : VALIDATOR) = struct
      type t = V.t
      let create x = if V.validate x then Some x else None
    end
    

    Rust (idiomatic newtypes)

    #[derive(Debug, Clone, PartialEq)]
    pub struct Email(String);
    
    impl Email {
        pub fn new(s: &str) -> Result<Self, &'static str> {
            if s.contains('@') { Ok(Email(s.to_owned())) }
            else { Err("invalid email: missing '@'") }
        }
        pub fn as_str(&self) -> &str { &self.0 }
    }
    
    // Typed IDs — same underlying u64, completely distinct types
    pub struct UserId(pub u64);
    pub struct ProductId(pub u64);
    

    Rust (generic validated wrapper — functor parallel)

    pub trait Validator<T> {
        type Error: fmt::Debug + fmt::Display;
        fn validate(value: &T) -> Result<(), Self::Error>;
    }
    
    pub struct Validated<T, V>(T, PhantomData<V>);
    
    impl<T, V: Validator<T>> Validated<T, V> {
        pub fn new(value: T) -> Result<Self, V::Error> {
            V::validate(&value)?;
            Ok(Validated(value, PhantomData))
        }
        pub fn inner(&self) -> &T { &self.0 }
    }
    
    pub struct Positive;
    impl Validator<i64> for Positive {
        type Error = String;
        fn validate(v: &i64) -> Result<(), String> {
            if *v > 0 { Ok(()) } else { Err(format!("{v} is not positive")) }
        }
    }
    
    pub type PositiveInt = Validated<i64, Positive>;
    

    Type Signatures

    ConceptOCamlRust
    Newtype definitiontype email = Email of stringstruct Email(String)
    Smart constructorval email_of_string : string -> email optionfn Email::new(s: &str) -> Result<Email, &'static str>
    Unwraplet string_of_email (Email s) = sfn as_str(&self) -> &str { &self.0 }
    Generic wrappermodule Validated (V : VALIDATOR)struct Validated<T, V>(T, PhantomData<V>)
    Validatormodule type VALIDATORtrait Validator<T>
    Transparent accessPattern match or accessorimpl Deref for T { type Target = Inner; }

    Key Insights

  • Zero cost — Rust newtypes (struct Foo(Bar)) are guaranteed by the compiler to have the
  • same memory layout as Bar. The abstraction is purely compile-time; no heap allocation, no indirection, no vtable.

  • Phantom types replace functors — OCaml uses functors (parameterised modules) to produce
  • specialised validated types. Rust achieves the same with generic structs and PhantomData<V>, where V is a zero-sized marker type carrying the validator logic as a trait impl.

  • **Deref for transparent delegation** — OCaml pattern-matches to extract the inner value.
  • Rust's Deref trait lets the newtype behave as its inner type for read-only operations (method calls, slice indexing) while still being a distinct type for function signatures.

  • **Result over option** — OCaml smart constructors naturally return 'a option. Rust
  • prefers Result<T, E> so callers get a machine-readable error; the ? operator then propagates it ergonomically through call stacks.

  • Type-level ID safety — Two u64 fields become UserId(u64) and ProductId(u64).
  • Passing a ProductId where a UserId is expected is a compile error with zero runtime cost — a guarantee OCaml's type aliases (type user_id = int) cannot provide because aliases are transparent to the type checker.

    When to Use Each Style

    **Use simple validated newtypes (Email, Username) when:** the invariant is specific to one type and you want an ergonomic new constructor with Display / Deref built in.

    **Use the generic Validated<T, V> wrapper when:** multiple types share the same validation shape (e.g. PositiveInt, NonEmptyStr, BoundedF64) and you want to define validators once and reuse them — the Rust analogue of an OCaml functor application.

    **Use typed ID newtypes (UserId, ProductId) when:** preventing accidental substitution of structurally identical primitive types is the primary goal and no validation logic is needed.

    Exercises

  • Create a NonEmptyString newtype that rejects empty strings at construction and implements Display and Deref<Target = str>.
  • Implement Validated<u8, RangeValidator> where RangeValidator checks that a number falls within [0, 100].
  • Write a SortedVec<T: Ord> newtype that wraps Vec<T> and guarantees sorted order after each insertion.
  • Open Source Repos