Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
218: The Newtype Pattern
I want to show you a bug that I've seen sneak into production more times than I'd care to admit. It's not a crash or a memory leak; it's a logic error that the compiler completely ignores because, as far as Rust is concerned, the code is perfectly valid.
struct Order {
id: u32,
customer_id: u32,
}
fn ship_order(order_id: u32, customer_id: u32) {
println!("Shipping order {} to customer {}", order_id, customer_id);
}
fn main() {
let order = Order { id: 101, customer_id: 5005 };
// Oops. I swapped the arguments.
ship_order(order.customer_id, order.id);
}
The Primitive Obsession Trap
In the example above, the code compiles without a single warning. Why? Because both the order ID and the customer ID are u32. To the compiler, they are identical. I've called this "Primitive Obsession"—relying on basic types to represent distinct domain concepts.
In a real system, this is a silent killer. You might accidentally pass a user_id into a function expecting a product_id, and instead of a compiler error, you get a runtime bug where a user is accidentally charged for the wrong item or, worse, sees someone else's private data.
Wrapping Primitives for Type Safety
We can fix this using the Newtype pattern. This involves creating a tuple struct with a single field. It sounds like a lot of boilerplate for something so simple, but it tells the compiler that these two u32 values are fundamentally different things.
struct OrderId(u32);
struct CustomerId(u32);
struct Order {
id: OrderId,
customer_id: CustomerId,
}
fn ship_order(order_id: OrderId, customer_id: CustomerId) {
println!("Shipping order {:?} to customer {:?}", order_id.0, customer_id.0);
}
fn main() {
let order = Order {
id: OrderId(101),
customer_id: CustomerId(5005)
};
// Now, if I try to swap them:
// ship_order(order.customer_id, order.id);
// ^-- The compiler now screams: "mismatched types"
ship_order(order.id, order.customer_id);
}
By wrapping the primitives, we've moved the error from runtime (where the customer gets the wrong package) to compile-time (where the developer gets a red squiggly line). I personally find this incredibly satisfying because it removes the mental overhead of double-checking argument orders in every function call.
Bypassing the Orphan Rule
There's another reason I use Newtypes: the Orphan Rule. In Rust, you can't implement an external trait for an external type. For example, you can't implement Display for Vec<T> because you didn't define the trait and you didn't define the Vec.
But if you wrap that Vec in a Newtype, it becomes your type. You now own it, which means you can implement any trait you want on it.
use std::fmt;
struct MyList(Vec<String>);
impl fmt::Display for MyList {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "My list contains {} items", self.0.len())
}
}
It's a lightweight wrapper that gives you full control over the type's behavior without the overhead of allocating a whole new data structure.
📋 Practical Task
Implementing a Type-Safe Currency Converter
You are building a financial tool that handles multiple currencies. Currently, the project uses f64 for everything, which is leading to bugs where developers are adding USD values to EUR values without converting them first.
Your Task:
- Create two Newtypes:
UsdandEur, both wrapping anf64. - Create a function
convert_usd_to_eur(amount: Usd) -> Eur(assume an exchange rate of 0.92). - Create a function
add_eur(a: Eur, b: Eur) -> Eurthat sums two Euro amounts. - In
main, attempt to pass aUsdvalue intoadd_eurand observe the compiler error. Then, fix it by using your conversion function first.
There are no comments for now.