Newtypes: Parse, Don’t Validate
A newtype wraps a primitive to give it identity and rules:
pub struct CanId(u16); // 11-bit standard CAN identifier
impl CanId {
pub fn new(raw: u16) -> Result<Self, InvalidCanId> {
if raw <= 0x7FF { Ok(Self(raw)) } else { Err(InvalidCanId(raw)) }
}
}
Outside its module, the constructor is the only way to create one, so any CanId that
exists is valid by construction, and no function that receives one needs
to check it again.
Two approaches to input checking:
- Validate: inspect the data, return
bool, and keep passing the raw type around. That the data was checked isn’t recorded anywhere the compiler can see. - Parse: inspect the data once, at the boundary, and produce a richer type as proof. The fact is now in the type system, visible to every function, every reviewer, and every generation run.
In review, follow any primitive (String, u16, f64) that crosses
more than one function boundary. If its validity is checked more
than once, or never, a newtype belongs there. The prompt follows directly:
“introduce CanId with a fallible constructor, and don’t pass raw u16
beyond the parse boundary.”