NewType Pattern
The NewType pattern isn't one of the classic Gang of Four design patterns, but it's a fundamental Rust idiom that provides type safety and semantic clarity. It's particularly powerful in Rust due to the language's zero-cost abstractions and strong type system.
What is the NewType Pattern?
The NewType pattern involves wrapping an existing type in a new struct to create a distinct type with the same underlying representation but different semantics. This provides:
- Type Safety: Prevents mixing up semantically different values of the same underlying type
- API Control: You can implement only the traits and methods you want to expose
- Zero Runtime Cost: The wrapper is eliminated at compile time
- Clear Intent: Makes code more self-documenting
Comparison with Java
In Java, you might use wrapper classes, but they have runtime overhead:
// Java - has runtime overhead
public class UserId {
private final int value;
public UserId(int value) {
this.value = value;
}
public int getValue() { return value; }
}
In Rust, the NewType pattern has zero runtime cost due to the compiler's optimizations.
Rust Implementation
use std::fmt; // NewType pattern - wrapping primitive types for type safety #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct UserId(u32); #[derive(Debug, Clone, PartialEq)] pub struct Email(String); // Implementing methods for NewType impl UserId { pub fn new(id: u32) -> Self { UserId(id) } pub fn as_u32(&self) -> u32 { self.0 } } impl Email { pub fn new(email: String) -> Result<Self, &'static str> { if email.contains('@') { Ok(Email(email)) } else { Err("Invalid email format") } } pub fn domain(&self) -> Option<&str> { self.0.split('@').nth(1) } pub fn as_str(&self) -> &str { &self.0 } } // Custom Display implementation impl fmt::Display for UserId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "User#{}", self.0) } } impl fmt::Display for Email { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } // Example usage demonstrating type safety pub struct User { id: UserId, email: Email, } impl User { pub fn new(id: u32, email: String) -> Result<Self, &'static str> { Ok(User { id: UserId::new(id), email: Email::new(email)?, }) } pub fn id(&self) -> UserId { self.id } pub fn email(&self) -> &Email { &self.email } } fn main() { // Create some NewType instances let user_id = UserId::new(123); let email = Email::new("user@example.com".to_string()).unwrap(); println!("User ID: {}", user_id); println!("Email domain: {:?}", email.domain()); // Create user let user = User::new(123, "alice@example.com".to_string()).unwrap(); // Demonstrate that NewTypes prevent accidental mixing let another_user_id = UserId::new(789); println!("User IDs can be compared: {}", user_id == another_user_id); println!("User ID as u32: {}", user_id.as_u32()); }
More Complex NewType Example
A more sophisticated example showing validation and behavior:
use std::fmt; // Example: A more complex NewType with validation and behavior #[derive(Debug, Clone, PartialEq)] pub struct Temperature(f64); impl Temperature { pub fn celsius(temp: f64) -> Self { Temperature(temp) } pub fn fahrenheit(temp: f64) -> Self { Temperature((temp - 32.0) * 5.0 / 9.0) } pub fn to_celsius(&self) -> f64 { self.0 } pub fn to_fahrenheit(&self) -> f64 { self.0 * 9.0 / 5.0 + 32.0 } pub fn to_kelvin(&self) -> f64 { self.0 + 273.15 } } impl fmt::Display for Temperature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.1}°C", self.0) } } fn main() { // Temperature example let temp_c = Temperature::celsius(25.0); let temp_f = Temperature::fahrenheit(77.0); println!("Temperature in Celsius: {}", temp_c); println!("Temperature in Fahrenheit: {:.1}°F", temp_c.to_fahrenheit()); println!("Temperature in Kelvin: {:.1}K", temp_c.to_kelvin()); }
Key Benefits Demonstrated
-
Type Safety:
UserIdtypes can't be accidentally swapped with other wrapped types, even though they both wrapu32 -
Zero Cost: The wrapper types are compiled away - no runtime overhead
-
Controlled API: You decide which operations are available (notice we don't derive
Addfor IDs) -
Validation: The
Emailtype enforces basic validation at construction time -
Semantic Clarity:
Temperature::celsius(25.0)is much clearer than just25.0
Advanced NewType Patterns
You can also implement traits selectively:
#![allow(unused)] fn main() { // Only implement specific traits you want impl std::ops::Add for Temperature { type Output = Temperature; fn add(self, other: Temperature) -> Temperature { Temperature(self.0 + other.0) } } }
Relation to Gang of Four
While not a GoF pattern, NewType often works with other patterns:
- Facade: NewTypes can simplify complex APIs
- Adapter: Wrap foreign types to match your interface
- Decorator: Add behavior to existing types without inheritance
The NewType pattern is particularly powerful in Rust because it leverages the type system for safety without runtime cost, something that's harder to achieve in classical OOP languages like Java.
Core Rust concepts introduced
1. Tuple Structs and the NewType Pattern
#![allow(unused)] fn main() { pub struct UserId(u32); }
- Tuple structs: Single-field structs with unnamed fields
- NewType pattern: Wrapping existing types for type safety
- Field access: Use
.0to access the wrapped value - Zero-cost abstraction: Wrapper is eliminated at compile time
2. Ownership and Borrowing
#![allow(unused)] fn main() { pub fn as_str(&self) -> &str { &self.0 // Borrowing the inner string data } }
- Ownership: Every value has exactly one owner
- Borrowing: Using
&to create references without taking ownership &self: Immutable borrow of the struct instance&str: String slice (borrowed view of string data)- Memory safety: Prevents use-after-free and data races at compile time
3. String Types
#![allow(unused)] fn main() { pub struct Email(String); // Owned string pub fn as_str(&self) -> &str // String slice (borrowed view) }
String: Owned, heap-allocated, growable string&str: Immutable string slice (view into string data)- Automatic conversion:
Stringcan be borrowed as&str - Memory efficiency: Pass
&strinstead ofStringwhen you don't need ownership
4. Derive Macros
#![allow(unused)] fn main() { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct UserId(u32); }
- Automatic trait implementations: Compiler generates code for common traits
Debug: Enables{:?}and{:#?}formatting for debuggingClone: Explicit copying with.clone()methodCopy: Implicit copying for simple stack-allocated typesPartialEq/Eq: Equality comparisons with==and!=Hash: Enables use inHashMap,HashSet, etc.
5. Result Type and Error Handling
#![allow(unused)] fn main() { pub fn new(email: String) -> Result<Self, &'static str> { if email.contains('@') { Ok(Email(email)) } else { Err("Invalid email format") } } }
Result<T, E>: Rust's approach to fallible operations- No exceptions: Errors are values, not thrown exceptions
- Explicit handling: Must handle both
Ok(value)andErr(error)cases - Type safety: Prevents forgotten error handling at compile time
6. Trait Implementation
#![allow(unused)] fn main() { impl fmt::Display for UserId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "User#{}", self.0) } } }
- Traits: Similar to interfaces, define shared behavior
impl Trait for Type: Syntax for implementing traits on types- Method signature:
&selffor methods that borrow the instance - Return types: Traits can specify return types like
fmt::Result
7. Associated Functions vs Methods
#![allow(unused)] fn main() { impl UserId { pub fn new(id: u32) -> Self { // Associated function UserId(id) } pub fn as_u32(&self) -> u32 { // Method self.0 } } }
- Associated functions: Don't take
self, called withType::function() - Methods: Take some form of
self, called withinstance.method() Selftype: Refers to the implementing type (here,UserId)- Constructor pattern:
new()is conventionally an associated function
8. Pattern Matching and Option Type
#![allow(unused)] fn main() { pub fn domain(&self) -> Option<&str> { self.0.split('@').nth(1) // Returns Option<&str> } }
Option<T>: Represents values that might not existSome(value): Contains a valueNone: Represents absence of value- No null pointers: Rust eliminates null pointer exceptions
- Explicit handling: Must handle both
SomeandNonecases
9. Visibility and Encapsulation
#![allow(unused)] fn main() { pub struct UserId(u32); // Public struct, but field is private by default }
pub: Makes items publicly visible- Default privacy: Struct fields are private unless marked
pub - Controlled access: Use methods to provide controlled access to data
- Encapsulation: Hide implementation details while exposing safe interfaces
10. Type Safety Without Runtime Cost
#![allow(unused)] fn main() { // This won't compile - different types despite same underlying data: // assert_eq!(user_id, another_different_type_id); // Compile error! }
- Strong typing: Types that wrap the same data are still distinct
- Compile-time guarantees: Type errors caught before runtime
- Zero-cost abstractions: Safety checks happen at compile time
- Performance: No runtime type checking needed
These fundamental concepts form the foundation for all Rust programming and will appear throughout the remaining pattern chapters. Understanding them here will make the subsequent patterns much clearer.