436: Newtype Derive Patterns
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
impl for newtypesderive_more::Display, derive_more::From, derive_more::Deref workResult/Option) vs. transparent newtypes#[derive(PartialOrd, Ord)] on newtypes provides comparison via the inner typeCode 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
Deref<Target=Inner> for transparency; OCaml provides only the functions defined in the module signature.pub fields expose the inner value directly; OCaml's abstract types require accessor functions.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);
}
}#[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
Exercises
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.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.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.