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<K> {
value: String,
_kind: PhantomData<K>,
}
impl<K> fmt::Display for Identifier<K> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.value)
}
}
impl<K> FromStr for Identifier<K> {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Identifier {
value: s.to_string(),
_kind: PhantomData,
})
}
}
struct User;
struct Group;
fn main() {
let u_id: Identifier<User> = "howdy".parse().unwrap();
let g_id: Identifier<Group> = "howdy".parse().unwrap();
// do_group_thing(&u_id); // Fails
do_group_thing(&g_id);
}
fn do_group_thing(id: &Identifier<Group>) {}
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<Group>`
found type `&Identifier<User>`
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.
There are several ways to deal with this kind of problem. The following solution is using the so-called newtype pattern, a unified trait for the object the newtype contains and a trait implementation for the newtype.
(Explanation is going to be inline, but if you'd like to see the code as a whole and at the same time test it then go to the playground.)
First, we create a trait that describes the minimal behaviour we'd like to see from an identifier. In Rust you don't have inheritance, you have composition, i.e. an object can implement any number of traits which will describe its behaviour. If you'd like to have something that is common in all your objects — which you would achieve via inheritance — then you have to implement the same trait for them.
use std::fmt;
trait Identifier {
fn value(&self) -> &str;
}
Then we create a newtype which contains a single value which is a generic type that is constrained to implement our Identifier
trait. The great thing about this pattern is that it will actually be optimised by the compiler at the end.
struct Id<T: Identifier>(T);
Now that we have a concrete type, we implement the Display
trait for it. Since Id
's internal object is an Identifier
, we can call the value
method on it so we only have to implement this trait once.
impl<T> fmt::Display for Id<T>
where
T: Identifier,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0.value())
}
}
The followings are definitions of different identifier types and their Identifier
trait implementations:
struct MyIdentifier(String);
impl Identifier for MyIdentifier {
fn value(&self) -> &str {
self.0.as_str()
}
}
struct MyUserIdentifier {
value: String,
user: String,
}
impl Identifier for MyUserIdentifier {
fn value(&self) -> &str {
self.value.as_str()
}
}
And last but not least, this is how you would use them:
fn main() {
let mid = Id(MyIdentifier("Hello".to_string()));
let uid = Id(MyUserIdentifier {
value: "World".to_string(),
user: "Cybran".to_string(),
});
println!("{}", mid);
println!("{}", uid);
}
The Display
was easy, however I don't think you could unify the FromStr
, as my example above demonstrates it is very likely that the different identifiers have different fields not just the value
(to be fair, some don't even have the value
, after all, the Identifier
trait only requires the object to implement a method called value
). And semantically the FromStr
supposed to construct a new instance from a string. Therefore I would implement FromStr
for all types separately.