ExamplesBy LevelBy TopicLearning Paths
436 Advanced

436: Newtype Derive Patterns

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "436: Newtype Derive Patterns" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Newtypes provide type safety but impose an implementation burden: you must re-derive or re-implement every trait the inner type has. Key difference from OCaml: 1. **Derive vs. module**: Rust newtypes derive traits selectively; OCaml modules hide the type and provide only explicitly exposed functions.

Tutorial

The Problem

Newtypes provide type safety but impose an implementation burden: you must re-derive or re-implement every trait the inner type has. Email(String) should support Display, FromStr, Deref<Target=str>, AsRef<str>, and more — all of which String already provides. The derive_more crate and custom macros generate these delegating implementations automatically. Without them, every newtype requires dozens of boilerplate impl blocks that simply forward to the inner type.

Newtype derive patterns appear in domain modeling (UserId, OrderId, Email), unit systems (Meters, Kilograms), and any codebase that uses newtypes extensively for type safety.

🎯 Learning Outcomes

  • • Understand the boilerplate burden of newtypes and how derive macros reduce it
  • • Learn which traits are derivable and which require custom impl for newtypes
  • • See how derive_more::Display, derive_more::From, derive_more::Deref work
  • • Understand validated newtypes (constructor returns Result/Option) vs. transparent newtypes
  • • Learn how #[derive(PartialOrd, Ord)] on newtypes provides comparison via the inner type
  • Code Example

    #![allow(clippy::all)]
    //! Newtype Derive Patterns
    //!
    //! Generating trait impls for newtypes.
    
    /// Newtype wrapper for validated email.
    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
    pub struct Email(String);
    
    impl Email {
        pub fn new(s: &str) -> Result<Self, &'static str> {
            if s.contains('@') {
                Ok(Email(s.to_string()))
            } else {
                Err("Invalid email")
            }
        }
    
        pub fn as_str(&self) -> &str {
            &self.0
        }
    }
    
    /// Newtype for positive integers.
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
    pub struct PositiveInt(u32);
    
    impl PositiveInt {
        pub fn new(n: u32) -> Option<Self> {
            if n > 0 {
                Some(PositiveInt(n))
            } else {
                None
            }
        }
    
        pub fn get(&self) -> u32 {
            self.0
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_email_valid() {
            let e = Email::new("test@example.com").unwrap();
            assert_eq!(e.as_str(), "test@example.com");
        }
    
        #[test]
        fn test_email_invalid() {
            assert!(Email::new("invalid").is_err());
        }
    
        #[test]
        fn test_positive_int_valid() {
            let p = PositiveInt::new(42).unwrap();
            assert_eq!(p.get(), 42);
        }
    
        #[test]
        fn test_positive_int_zero() {
            assert!(PositiveInt::new(0).is_none());
        }
    
        #[test]
        fn test_positive_int_ord() {
            let a = PositiveInt::new(1).unwrap();
            let b = PositiveInt::new(2).unwrap();
            assert!(a < b);
        }
    }

    Key Differences

  • Derive vs. module: Rust newtypes derive traits selectively; OCaml modules hide the type and provide only explicitly exposed functions.
  • Deref transparency: Rust newtypes can implement Deref<Target=Inner> for transparency; OCaml provides only the functions defined in the module signature.
  • Field access: Rust newtypes with pub fields expose the inner value directly; OCaml's abstract types require accessor functions.
  • Derive more: The derive_more crate reduces newtype boilerplate; OCaml's ppx_deriving serves the same role.
  • OCaml Approach

    OCaml's newtype equivalent is an abstract type in a module: module Email : sig type t; val of_string : string -> t option; val to_string : t -> string end. The module hides the string inside, requiring explicit conversion. OCaml's ppx_deriving can generate comparison and hash functions. The module system enforces the type boundary more strictly than Rust's newtypes, which are just single-field structs.

    Full Source

    #![allow(clippy::all)]
    //! Newtype Derive Patterns
    //!
    //! Generating trait impls for newtypes.
    
    /// Newtype wrapper for validated email.
    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
    pub struct Email(String);
    
    impl Email {
        pub fn new(s: &str) -> Result<Self, &'static str> {
            if s.contains('@') {
                Ok(Email(s.to_string()))
            } else {
                Err("Invalid email")
            }
        }
    
        pub fn as_str(&self) -> &str {
            &self.0
        }
    }
    
    /// Newtype for positive integers.
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
    pub struct PositiveInt(u32);
    
    impl PositiveInt {
        pub fn new(n: u32) -> Option<Self> {
            if n > 0 {
                Some(PositiveInt(n))
            } else {
                None
            }
        }
    
        pub fn get(&self) -> u32 {
            self.0
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_email_valid() {
            let e = Email::new("test@example.com").unwrap();
            assert_eq!(e.as_str(), "test@example.com");
        }
    
        #[test]
        fn test_email_invalid() {
            assert!(Email::new("invalid").is_err());
        }
    
        #[test]
        fn test_positive_int_valid() {
            let p = PositiveInt::new(42).unwrap();
            assert_eq!(p.get(), 42);
        }
    
        #[test]
        fn test_positive_int_zero() {
            assert!(PositiveInt::new(0).is_none());
        }
    
        #[test]
        fn test_positive_int_ord() {
            let a = PositiveInt::new(1).unwrap();
            let b = PositiveInt::new(2).unwrap();
            assert!(a < b);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_email_valid() {
            let e = Email::new("test@example.com").unwrap();
            assert_eq!(e.as_str(), "test@example.com");
        }
    
        #[test]
        fn test_email_invalid() {
            assert!(Email::new("invalid").is_err());
        }
    
        #[test]
        fn test_positive_int_valid() {
            let p = PositiveInt::new(42).unwrap();
            assert_eq!(p.get(), 42);
        }
    
        #[test]
        fn test_positive_int_zero() {
            assert!(PositiveInt::new(0).is_none());
        }
    
        #[test]
        fn test_positive_int_ord() {
            let a = PositiveInt::new(1).unwrap();
            let b = PositiveInt::new(2).unwrap();
            assert!(a < b);
        }
    }

    Deep Comparison

    OCaml vs Rust: macro newtype derive

    See example.rs and example.ml for side-by-side implementations.

    Key Points

  • Rust macros operate at compile time
  • OCaml uses ppx for similar metaprogramming
  • Both languages support powerful code generation
  • Rust's macro_rules! is built into the language
  • OCaml's approach requires external tooling
  • Exercises

  • derive_more usage: Add derive_more as a dependency and rewrite Email using #[derive(Display, From, Deref, Into)]. Remove the manual as_str method and verify the same functionality works through Deref.
  • Newtype stack: Create a stack of newtypes: RawId(u64), UserId(RawId), AdminId(UserId). Implement From<u64> for UserId via RawId. Show that AdminId can't be accidentally used where UserId is expected.
  • Comprehensive newtype: Implement Percentage(f64) with validation (0.0..=100.0), Display showing 42.5%, Add/Sub operations that clamp results to valid range, and From<f64> with saturation.
  • Open Source Repos