I have a basic type with some functionality, including trait implementations:
use std::fmt;
use std::str::FromStr;
pub struct MyIdentifier {
value: String,
Use a PhantomData to add a type parameter to your Identifier
. This allows you to "brand" a given identifier:
use std::{fmt, marker::PhantomData, str::FromStr};
pub struct Identifier {
value: String,
_kind: PhantomData,
}
impl fmt::Display for Identifier {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.value)
}
}
impl FromStr for Identifier {
type Err = ();
fn from_str(s: &str) -> Result {
Ok(Identifier {
value: s.to_string(),
_kind: PhantomData,
})
}
}
struct User;
struct Group;
fn main() {
let u_id: Identifier = "howdy".parse().unwrap();
let g_id: Identifier = "howdy".parse().unwrap();
// do_group_thing(&u_id); // Fails
do_group_thing(&g_id);
}
fn do_group_thing(id: &Identifier) {}
error[E0308]: mismatched types
--> src/main.rs:32:20
|
32 | do_group_thing(&u_id);
| ^^^^^ expected struct `Group`, found struct `User`
|
= note: expected type `&Identifier`
found type `&Identifier`
The above isn't how I'd actually do it myself, though.
I want to introduce two types which have the same fields and behaviour
Two types shouldn't have the same behavior — those should be the same type.
I don't want to copy the entire code I just wrote, I want to reuse it instead
Then just reuse it. We reuse types like String
and Vec
all the time by composing them as part of our larger types. These types don't act like String
s or Vec
s, they just use them.
Maybe an identifier is a primitive type in your domain, and it should exist. Create types like User
or Group
and pass around (references to) users or groups. You certainly can add type safety, but it does come at some programmer expense.