Has-A relationship in Rust

Migo·2024년 6월 16일
0

Fluent Rust

목록 보기
20/23

Problem

Say we have a characters and weapon. Each character has their own way to fight with weapon. From time to time, they change their weapon(perhaps because it got RUSTy). Then the trait for that will be something along the lines of:

trait TCharacter {
    fn fight(&self);
	
    fn set_weapon<T: TWeapon>(&mut self, with_weapon: T);
}

trait TWeapon: std::fmt::Debug {
    fn use_weapon(&self);
}

Here, the trait design seems perfectly benign. Or doesn't it?

Implementation

Let's implement King

#[derive(Debug)]
struct King {
    weapon: Box<dyn TWeapon>,
}

King has a weapon as a Box<dyn TWeapon>, which is a trait object. This is a good design if the weapon can change during the lifespan of the king. So, you can implement TCharacter and TWeapon as follows:

impl TCharacter for King {
    fn fight(&self) {
        println!("king is fighting...");
        self.weapon.use_weapon();
    }

    fn set_weapon<T: TWeapon + 'static>(&mut self, weapon: T) {
        self.weapon = Box::new(weapon);
    }
}

Okay, with this design, you successfully encapsulate HOW weapon is used by achieving HAS-A relationship between King and TWeapon. As you can see here, King is not aware of what kind of weapon it will receive.


Things will not work with generic

Now, let's try to implement Knight but in this time with generic:

#[derive(Debug)]
struct Knight<T: TWeapon> {
    weapon: T,
}

Okay, Knight HAS whatever the type that implements TWeapon. But then ramification of this is:

impl<T: TWeapon> TCharacter for Knight<T> {
    fn fight(&self) {
        println!("knight is fighting...");
        self.weapon.use_weapon();
    }

    fn set_weapon<U: TWeapon>(&mut self, with_weapon: U) {
        self.weapon = with_weapon; // ERROR!  expected type parameter `T`
    }
}

For experienced Rust devs, this is obvious because what we are trying to do is we want to set weapon to Knight<T> while given with_weapon is of type U. If we allow them, the type of receiver(self) will become Knight<U> But then there is no sign for ownership transfer.

If we pretend that it wasn't for TCharacter trait implemenation, the way you change the state of Knight will be:

impl<T: TWeapon> Knight<T> {
    fn set_weapon<U: TWeapon>(self, with_weapon: U) -> Knight<U> {
        Knight {
            weapon: with_weapon,
        }
    }
}

But again, our task is to implement TCharacter.

Unfortunately, you cannot make it without changing either of the following:

  • change the field of Knight
  • remove the set_weapon and implement them for each structs

At this point, you may be thinking it doesn't make sense that you have to remove or change the field type. But I guarantee you, you cannot make generic struct that HAS TWeapon and also implmenets TCharacter as it is now.

For those curious, let's see the next futile attempts:

Tries

Try 1: introducing ownership transfer and associated type:

trait TCharacter {
    type Weapon; // associated type
    
    ...

	//ERROR associated item referring to unboxed trait object for its own trait
    fn set_weapon<T: TWeapon + 'static>(self, with_weapon: T) -> TCharacter<Weapon = T>; 
}

Try 2: Use of generic trait

trait TCharacter<T: TWeapon> {
     ...
    
	//ERROR associated item referring to unboxed trait object for its own trait
    fn set_weapon<U: TWeapon>(self, with_weapon: U) -> TCharacter<U>;
}

Try 3: Return Self - Returning Self means that receiver and return type INCLUDING its generic type must be the time. Obviously you cannot make it.


Try 4: Returning Box<dyn TCharacter>


trait TCharacter<T: TWeapon> {
	//ERROR the trait `designs::strategy::interfaces::TCharacter` cannot be made into an object
    fn set_weapon<U: TWeapon>(self, with_weapon: U) -> Box<dyn TCharacter<U>>;
}




Conclusion

In the end, it all comes down to object safety and type safety. Use of generic struct for HAS-A relationship in Rust means that the lifespan as type of the two - HAVES and HAVEE are coupled.

Note that I'm using the word lifespan as opposed to lifetime as it signifies utterly different thing.

profile
Dude with existential crisis

1개의 댓글

comment-user-thumbnail
2025년 5월 6일

A relationship is a dynamic connection between two individuals, based on trust, love, and mutual understanding. While every relationship is unique, many people seek ways to understand their compatibility better. Tools like a free zodiac relationship calculator are becoming popular for gaining insights into astrological compatibility. These calculators help individuals assess how their zodiac signs align, offering a fun way to explore potential strengths and challenges in their bond. However, while astrology can provide interesting perspectives, the foundation of a successful relationship lies in communication and respect.

답글 달기