Some years ago I read this great article https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ which explains the idea of "type-driven development" by using an example in which you can either validate user input over and over again or parse it one time and use the type system to prove that it has already been validated.
Rust's type system is much weaker than haskell, but is still good enough to implement many of the ideas coming from the functional programming world.
Say you get some user input and need to ensure it's not blank, is under a
certain length, and doesn't include certain characters. You could write a
function that takes your string and returns a boolean. In dynamically typed
languages this is not only standard, but is often the only option. In languages
with a good type system you could instead write a function that takes a string
and returns a custom Username type. If the only way to create a username type
is your Parse constructor then you now have a string which is proven to be
validated.
pub struct SubscriberName(String);
impl SubscriberName {
pub fn parse(s: String) -> SubscriberName {
let is_empty_or_whitespace = s.trim().is_empty();
let is_too_long = s.graphemes(true).count() > 256;
let forbidden_characters =
['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
let contains_forbidden_characters =
s.chars().any(|g| forbidden_characters.contains(&g));
if is_empty_or_whitespace
|| is_too_long
|| contains_forbidden_characters
{
panic!("{} is not a valid subscriber name.", s)
} else {
Self(s)
}
}
}
impl AsRef<str> for SubscriberName {
fn as_ref(&self) -> &str {
&self.0
}
}
This code has the ability to parse a string into a new SubscriberName type
which proves the input has been validated. We've also implemented AsRef<str>
so that SubscriberName can be elegantly converted to a immutable reference to
the string it contains when it needs to be used as a string.