Semantic Confusion
When a function takes multiple arguments of the same type, call sites are unclear:
#![allow(unused)] fn main() { struct LoginError; pub fn login(username: &str, password: &str) -> Result<(), LoginError> { // [...] Ok(()) } let password = "password"; let username = "username"; // In another part of the codebase, we swap arguments by mistake. // Bug (best case), security vulnerability (worst case) login(password, username); }
The newtype pattern can prevent this class of errors at compile time:
#![allow(unused)] fn main() { pub struct Username(String); pub struct Password(String); struct LoginError; pub fn login(username: &Username, password: &Password) -> Result<(), LoginError> { // [...] Ok(()) } let password = Password("password".into()); let username = Username("username".into()); login(password, username); // 🛠️❌ }
-
Run both examples to show students the successful compilation for the original example, and the compiler error returned by the modified example.
-
Stress the semantic angle. The newtype pattern should be leveraged to use distinct types for distinct concepts, thus ruling out this class of errors entirely.
-
Nonetheless, note that there are legitimate scenarios where a function may take multiple arguments of the same type. In those scenarios, if correctness is of paramount importance, consider using a struct with named fields as input:
#![allow(unused)] fn main() { pub struct LoginArguments<'a> { pub username: &'a str, pub password: &'a str, } fn login(i: LoginArguments) {} let password = "password"; let username = "username"; // No need to check the definition of the `login` function to spot the issue. login(LoginArguments { username: password, password: username, }) }
Users are forced, at the callsite, to assign values to each field, thus increasing the likelihood of spotting bugs.