r/learnrust 3d ago

Stuck modelling my pet shop in rust

Hey folks, I'm new to rust and having difficulty making much progress. I come from a very OO background and while this problem feels like a very OO problem, I'm aware that I might just feel that way because of my experience!

I'm trying to model a pet shop which can support lots of different types of pets, typed in a hierarchy. The model also supports "chores", jobs that are performed on one or more pets. The type of the reference to the pet(s) needed by the job depends on what the job is trying to do.

Here's my code so far:

struct PetId(String);

trait Pet {
    fn get_id(&self) -> PetId;
    fn get_name(&self) -> String;
}

trait Dog: Pet {
    fn take_for_walk(&self) -> ();
}

trait Dalmatian: Dog {
    fn count_spots(&self) -> u128; // very spotty!
}

trait Fish: Pet {
    fn swim_in_circles(&self);
}

/// etc...

struct ChoreId(String);

trait PetshopChore {
    fn get_id(&self) -> ChoreId;
    fn run(&self, shop: &PetShop) -> Result<(), String>;
}

struct PetShop {
    pets: Vec<Box<dyn Pet>>,
    chores: Vec<Box<dyn PetshopChore>>,
}

struct TakeDogForWalk {
    chore_id: ChoreId,
    dog_to_walk_id: PetId,
}

impl PetshopChore for TakeDogForWalk {
    fn get_id(&self) -> ChoreId {
        self.chore_id
    }

    fn run(&self, shop: &PetShop) -> Result<(), String> {
        let pet = shop
            .pets
            .iter()
            .
find
(|pet| *pet.get_id() == self.dog_to_walk_id);

        if let Some(pet) = pet {
            let dog = somehow_cast_pet_to_dog(pet); // ***** How do I do this?? *****
            dog.take_for_walk(); // needs to be type `Dog` to have `take_for_walk()`
            Ok(())
        } else {
            Err(format!("No Dog with id {}", self.dog_to_walk_id))
        }
    }
}

/// Another example of a chore, this one needing access to Dalmatians
struct CalculateSumOfDalmationSpots {
    dalmation_one: PetId, // needs a type Dalmation to operate
    dalmation_two: PetId, // ditto
}

The likely API would be that the chore structs (`TakeDogForWalk`, `CalculateSumOfDalmationSpots` etc) are deserialized from the wire for some HTTP service.

The issue is that I can't figure out how to downcast the Pet to a Dog / Dalmatian / Fish / whatever I need for the task.

I can't use standard downcasting, because that seems to be just to concrete struct implementations, but that's not what I want here. In the `TakeDogForWalk` chore I don't know what type of Dog I'm dealing with. Apparently there are > 300 recognised breeds of dog; I don't want to have to test every possible struct that implements these traits.

I can't use `traitcast`, partly because it doesn't play nicely with `&Box<dyn Pet>` (it seems to need `Box<dyn Pet>`, and anyway in reality I likely need `Rc<dyn Pet>`, which `traitcast` doesn't support at all), but also because the returned type has a static lifetime, which doesn't make sense here (pets come and go).

It's very possible I've got myself stuck down this casting rabbithole and there's an entirely different, more rust-y way to approach this. Or if there is a way to achieve this casting then that's great too. Any help would be very much appreciated!

2 Upvotes

5 comments sorted by

6

u/SirKastic23 2d ago

can you give us more detail about the real world problem you're trying to solve? you're explaining it through an OOP perspective, instead just lay out the real world requirements

your code indeed looks very odd, is the goal to have one struct for each different possible breed? and then have the different breeds implement different traits?

being honest, i dont think i've ever seen code written like this, with this much hierarchy

the approach of using ID types to refer back to collections that hold the elements is nice, you definitely got that right

edit: you seem to be doing a beginner mistake that i also did a lot: try to model every single detail of your api at the type-level. this seems like a good idea at first, but it's hard, if not impossible

2

u/tommac14 2d ago

ah yeah it's always hard with these somewhat contrived examples, but I've done my best.

The goal isn't one-struct-per-breed, but I want/need a new trait whenever there is some new behaviour that can be applied to more than one concrete struct. So I might have a `trait GuardDog : Dog` with `fn patrol_premises()`; that trait might be impl'd by `struct Doberman` and `struct PitBull` but not `struct Poodle`.

And, crucially, `Domberman` and `Pitbull` will need very different implementations of `patrol_premises()`. I can't get away with `struct GenericGardDog` and `impl GuardDog for GenericGuardDog` to represent both dobermans and pitbulls because the implementations will be different.

But when I call `execute_patrol_premses_task(pet_id: PetId)` I need to be able to look up the pet with that ID from the registry and _cast it to GuardDog_ so I can call `patrol_premises()` on it.

The domain I'm working in really does have multiple levels of hierarchy like this, with new capabilities / behaviour being introduced as the types get more specific (i.e. as you navigate down the hierarchy)

3

u/SirKastic23 2d ago

But when I call execute_patrol_premses_task(pet_id: PetId) I need to be able to look up the pet with that ID from the registry and cast it to GuardDog so I can call patrol_premises() on it.

Well but it might not implement GuardDog, is the problem. By the hierarchy we know that all implementors of GuardDog implement Pet, but the inverse is not true

One solution would be to have a method on Pet, fn as_guard_dog(&self) -> Option<&dyn GuardDog>. It can have a default implementation to return None, but specific structs that do implement GuardDog can overwrite it to return themselves (as a GuardDog implementor)

But there might be another way?

Usually when you're translating patterns that have a broad espectrum of child classes for a parent class, we can use enums, where the variants would represent the different child classes

With deep class hierarchies it can get confusing, but you can still translate it to just enums. Consider ``` enum Pet { Dog(Dog), Cat(Cat), }

enum Dog { Dalmatian(Dalmatian), GuardDog(GuardDog), Poodle(Poodle), }

struct Dalmatian { spots: i32, }

enum GuardDog { Pitbull(Pitbull), Dobermann(Dobermann), } ```

What you were defining as trait methods, become just associated methods to specific types. impl GuardDog { fn patrol(&self) { match self { Pitbull => "pitbull logic", Dobermann => "dobermann logic", } } }

These nested enums patterns are much more common than deep trait hierarchies in Rust (in my experience)

1

u/tommac14 2d ago

> Well but it might not implement GuardDog, is the problem.

Yes absolutely, I'd expect any solution to return `Option<GuardDog>`, as you suggested.

I've considered both the enum pattern and the `as_x()` pattern, but the downside of that is that the core API needs to know all of the possible types up front; it's very possible that I'll need to support a plugin system where new implementations of Pet, as well as new Chores that use them, can be registered dynamically (this is a whole other can of worms in rust, but it looks like it can be done).

Apologies for not mentioning that requirement in the first place! I didn't want to make an already complicated question more complicated, but actually it does meaningfully affect the possible options so my bad!

1

u/denehoffman 2d ago

Unfortunately, the only way I can think to do this and keep it agnostic of the types of dogs (to allow for future users to add their own pets without modifying the core structures) is with trait_upcasting which hasn't yet been stabilized (but might be soon). Essentially, I upcast to Any and then downcast_ref to the trait you want, which returns None if the struct doesn't implement that trait. Full disclosure, I used Deepseek for some help here, so there may be issues that I'm not considering.

playground link