Step 2 of 7
Newtypes: give plain values a meaning
In 1999 the Mars Climate Orbiter was lost because one team's software produced pound-force seconds and another's expected newton seconds. Both were just numbers. In your code it's fn transfer(from: u64, to: u64, amount: u64): nothing stops a caller from swapping from and to.
A newtype is a tuple struct with one field that gives a plain value its own type:
struct UserId(u64);
struct OrderId(u64);
fn cancel_order(user: UserId, order: OrderId) {
println!("user {} cancels order {}", user.0, order.0);
}
fn main() {
let user = UserId(7);
let order = OrderId(1001);
cancel_order(order, user);
}
error[E0308]: arguments to this function are incorrect
The mix-up is now a compile error, and rustc even notices the two arguments are swapped and suggests the fix. Newtypes cost nothing at run time: a UserId is laid out exactly like a u64.
Parse, don't validate
A newtype with a private field goes further: if the only way to create one is a function that checks the input, then every value of that type is valid, everywhere, forever. Functions that take an Email never need to check it again.
mod email {
pub struct Email(String);
impl Email {
pub fn parse(s: &str) -> Result<Email, String> {
let s = s.trim();
match s.split_once('@') {
Some((user, domain)) if !user.is_empty() && domain.contains('.') => Ok(Email(s.to_lowercase())),
_ => Err(format!("invalid email: {s:?}")),
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
}
use email::Email;
fn send_welcome(to: &Email) -> String {
format!("welcome mail sent to {}", to.as_str())
}
fn main() {
for input in [" [email protected] ", "not-an-email", "@arena.dev"] {
match Email::parse(input) {
Ok(e) => println!("{}", send_welcome(&e)),
Err(e) => println!("{e}"),
}
}
}
welcome mail sent to [email protected]
invalid email: "not-an-email"
invalid email: "@arena.dev"
send_welcome can't receive an unchecked string: outside the email module, Email(String::from("oops")) doesn't compile (error[E0603]: tuple struct constructor `Email` is private). The validation happens once, at the edge of the program, which is the idea behind the slogan parse, don't validate.
What newtypes are good for
- Ids and units:
UserId,OrderId,Meters,Cents. Implement only the operations that make sense (Cents + Centsyes,Cents * Centsno). - Validated values:
Email,Username,NonEmptyString,Port. - The orphan rule: you can't implement a foreign trait for a foreign type (
impl Display for Vec<String>is refused), but you can for your newtype around it.
Derive what the type needs (Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord) and add accessors like as_str or value. Avoid implementing Deref just to "inherit" all the inner type's methods: it brings back the operations the newtype was meant to hide.
Common mistakes
- Money as
f64.0.1 + 0.2 != 0.3in floating point. Store whole cents in an integer newtype. - A public field (
pub struct Email(pub String)) lets anyone build an unchecked value, defeating the point. .0everywhere: if callers constantly unwrap the newtype, give it the methods they need instead.
Your task
Write Cents, a money type. Cents::parse(s) accepts dollar amounts like "12.34", "12.3", "5" or "$5" (an optional leading $) and returns the amount in cents. Anything else (empty, a minus sign, no digit before the ., more than two decimals, letters) is Err(format!("invalid amount {s:?}")). Implement Add so two Cents can be added, and Display so Cents(1234) prints $12.34.
Challenge 2
Stop ids from being mixed up. Create newtypes UserId(u64) and OrderId(u64) (derive Debug, Clone, Copy, PartialEq, Eq, Hash) and change cancel to take them, so cancel(order, user) no longer compiles. It returns user 7 cancelled order 1001 and records the cancellation in the set.
Challenge 3
Use a newtype to get around the orphan rule. Table wraps rows of cells. Implement Display so each row prints its cells joined by |, rows separated by newlines (no newline at the end), and FromStr that parses the same format back (split rows on \n, cells on |, trimming each cell). Parsing never fails, so use type Err = std::convert::Infallible;.
Previous: Constructors, Default and builders Next: Make invalid states unrepresentable