Step 5 of 7
Newtypes, the orphan rule and type aliases
The orphan rule
You may implement a trait for a type only if the trait or the type is defined in your crate. This is the orphan rule. You can't implement std's Display for std's Vec:
use std::fmt;
impl fmt::Display for Vec<String> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.join(", "))
}
}
fn main() {}
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
Why so strict? Imagine two crates you depend on both wrote impl Display for Vec<String>, differently. Which one should your program use? The orphan rule (part of coherence) guarantees that can never happen, so adding a dependency can't break your build with clashing impls.
The newtype pattern
The fix is a newtype: a tuple struct with one field that wraps the foreign type. The wrapper is yours, so you may implement any trait for it.
use std::fmt;
struct Scores(Vec<u32>);
impl fmt::Display for Scores {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.0.iter().max() {
Some(best) => write!(f, "{} scores, best {}", self.0.len(), best),
None => write!(f, "no scores yet"),
}
}
}
fn main() {
let s = Scores(vec![72, 98, 85]);
println!("{s}");
println!("{}", Scores(Vec::new()));
println!("{}", std::mem::size_of::<Scores>() == std::mem::size_of::<Vec<u32>>());
}
3 scores, best 98
no scores yet
true
The last line shows newtypes are free: Scores is exactly the size of the Vec inside, and the compiler removes the wrapper entirely. You reach the inner value with .0. (You could implement Deref to forward every Vec method, but that hides the wrapper; most teams prefer exposing just the methods they need.)
Newtypes for safety
The other big use: give plain numbers and strings distinct types so they can't be mixed up.
#[derive(Debug, Clone, Copy, PartialEq)]
struct UserId(u32);
#[derive(Debug, Clone, Copy, PartialEq)]
struct OrderId(u32);
fn cancel(order: OrderId, by: UserId) -> String {
format!("{:?} cancelled by {:?}", order, by)
}
fn main() {
let user = UserId(7);
let order = OrderId(1042);
println!("{}", cancel(user, order));
}
error[E0308]: arguments to this function are incorrect
With two bare u32 parameters, swapping them would compile and quietly cancel the wrong order.
Type aliases: a new name, not a new type
A type alias gives an existing type a shorter name. It's purely for readability: the alias and the original are interchangeable, so it gives no safety at all.
use std::collections::BTreeMap;
type Inventory = BTreeMap<String, u32>;
type Result<T> = std::result::Result<T, String>;
fn take(stock: &mut Inventory, item: &str, qty: u32) -> Result<u32> {
let have = stock.get_mut(item).ok_or(format!("no {item} in stock"))?;
if *have < qty {
return Err(format!("only {have} {item} left"));
}
*have -= qty;
Ok(*have)
}
fn main() {
let mut stock = Inventory::new();
stock.insert("potion".to_string(), 5);
println!("{:?}", take(&mut stock, "potion", 2));
println!("{:?}", take(&mut stock, "potion", 9));
println!("{:?}", take(&mut stock, "sword", 1));
}
Ok(3)
Err("only 3 potion left")
Err("no sword in stock")
The type Result<T> = std::result::Result<T, MyError>; pattern is everywhere: std::io::Result<T>, std::fmt::Result, anyhow::Result<T>. Each module that has one error type defines one, so signatures stay short.
newtype struct UserId(u32) |
alias type UserId = u32 |
|
|---|---|---|
| new type? | yes, can't be mixed up | no, it's just u32 |
| implement foreign traits? | yes | no (it's still the foreign type) |
| runtime cost | none | none |
Your task
Tags wraps a Vec<String>, and you can't implement Display for Vec<String> directly. Implement fmt::Display for Tags: each tag gets a # in front and they're separated by single spaces (#rust #crab). With no tags, display (no tags).
Challenge 2
Define a type alias ParseResult<T> for a Result whose error type is String, so the two signatures below compile. Then write both functions:
parse_port("8080")isOk(8080). If the text isn't a validu16, returnErr("not a number: <text>"). Port0isErr("port 0 is reserved").parse_addr("localhost:8080")isOk(("localhost".to_string(), 8080)). Without a:, returnErr("missing ':' in <text>"). Useparse_port(and?) for the port part.
Challenge 3
Use newtypes so user ids and order ids can't be mixed up. Define UserId(u32) and OrderId(u32), both deriving Debug, Clone, Copy, PartialEq. Then write:
orders_of(orders, user): the ids of the orders that belong touser, in their original order.total_spent(orders, user): the sum oftotal_centsof that user's orders.
Previous: Blanket impls and extension traits Next: PhantomData and typestate