Search code examples
ooprustinterior-mutability

OOP in Rust and shared/mutable references


In the context of a series of programming lessons, I have decided to use Rust instead of C++ as the support programming language. One aspect of these lessons is dedicated to OOP (dynamic dispatch) relying on interfaces (dyn traits): composition of minimal interfaces instead of deep inheritance trees. I know OOP does not fit well with modern languages and approaches, but the existing codebase and the habits of the teams since the 90s are still so present that the students must be at least aware of this paradigm (even if we don't encourage its usage for new developments).

In this playground is shown a minimal example inspired from an exercise formerly done in C++ (many other things exist around this excerpt). At the abstract level, an Entity has an internal state (a position here, to keep it simple) and several dynamic components responsible for various behaviours (drawing, animation, reaction to events...). These dynamic components implement some predefined interfaces (dyn traits) and can be freely defined at the application level (the abstract level does not have to know the details of these components). Some of these components can have some internal data which could even be mutated. For example, in this minimal code, a Shape component if mainly dedicated to drawing (no mutable operation is required for the entity or this component in general), but an Animator component can cause mutations on the entity (let's say its position), on the component itself and even on other components (change the color of the next drawing for example).

As requested in a comment, here is the code inline:

mod common {
    pub trait Shape {
        fn draw(
            &self,
            entity: &Entity,
        );
        fn change_color(
            &mut self,
            color: String,
        );
    }

    pub trait Animator {
        fn animate(
            &mut self,
            entity: &mut Entity,
        );
    }

    #[derive(Debug)]
    pub struct Pos {
        pub x: f64,
        pub y: f64,
    }

    pub struct Entity {
        pos: Pos,
        shape: Box<dyn Shape>,
        animator: Box<dyn Animator>,
    }

    impl Entity {
        pub fn new(
            pos: Pos,
            shape: Box<dyn Shape>,
            animator: Box<dyn Animator>,
        ) -> Self {
            Self {
                pos,
                shape,
                animator,
            }
        }

        pub fn pos(&self) -> &Pos {
            &self.pos
        }

        pub fn pos_mut(&mut self) -> &mut Pos {
            &mut self.pos
        }

        pub fn change_color(
            &mut self,
            color: String,
        ) {
            self.shape.change_color(color);
        }

        pub fn draw(&self) {
            self.shape.draw(self);
        }

        pub fn animate(&mut self) {
            let anim = &mut self.animator;
            anim.animate(self);
        }
    }
}

mod custom {
    use super::common::{Animator, Entity, Shape};

    pub struct MyShape {
        color: String,
    }
    impl MyShape {
        pub fn new(color: String) -> Self {
            Self { color }
        }
    }
    impl Shape for MyShape {
        fn draw(
            &self,
            entity: &Entity,
        ) {
            println!("draw at {:?} with {:?}", entity.pos(), self.color);
        }
        fn change_color(
            &mut self,
            color: String,
        ) {
            self.color = color;
        }
    }

    pub struct MyAnim {
        count: i32,
    }
    impl MyAnim {
        pub fn new() -> Self {
            Self { count: 0 }
        }
    }
    impl Animator for MyAnim {
        fn animate(
            &mut self,
            entity: &mut Entity,
        ) {
            let pos = entity.pos_mut();
            if (self.count % 2) == 0 {
                pos.x += 0.1;
                pos.y += 0.2;
            } else {
                pos.x += 0.2;
                pos.y += 0.1;
            }
            self.count += 1;
            if self.count >= 3 {
                entity.change_color("red".to_owned());
            }
        }
    }
}

fn main() {
    use common::{Entity, Pos};
    use custom::{MyAnim, MyShape};
    let mut entity = Entity::new(
        Pos { x: 0.0, y: 0.0 },
        Box::new(MyShape::new("green".to_owned())),
        Box::new(MyAnim::new()),
    );
    entity.draw();
    for _ in 0..5 {
        entity.animate();
        entity.draw();
    }
}

As you can see, the provided code cannot be compiled since, at line 66, anim is a mutable reference to the Animator component responsible for the dynamic dispatch but the parameter of the method is also a mutable reference to the Entity as a whole which contains the previous Animator. This parameter is needed if we want the Animator to be able to make changes on the entity. I'm stuck with this situation and I can only think about workarounds that look quite ugly to me:

  • don't pass the entity as a parameter but each of its field (except the animator) as many parameters: what's the point of defining structs then? (if an entity is made of twelve fields, should I pass eleven parameters every time I would act on this entity?)
  • embed each field of an entity in a RefCell and pretend every parameter of every function is a non-mutable reference, then borrow_mut() everywhere we want to and hope it won't panic: for me, it's like giving-up the idea that function prototypes tell and enforce the intent of the code (let's add some Rc everywhere in order to totally forget who owns what, and we obtain Java ;^)

I'm certain I did some bad choices about what deserves to be exclusive (&mut) or shared (&), but I can't see a reasonable limit. In my opinion, when an entity has to be animated, it's its own concern: there is nothing to be shared, except looking at the state of the surrounding environment (but not changing it). If we share everything and rely on interior-mutability in order to enable safe mutations at run-time (thanks to the ref-count) it sounds to me like: «let's drive like crazy, as if there were no traffic regulations, as long as no one complains (try_borrow()/try_borrow_mut()) and we don't have any accident (panic!())».

Could anyone suggest a better organisation of my structs/functions in order to enable the intended behaviour: an entity made of a few dynamic (as in OOP) components responsible for the details of the actions on the concerned entity?


Solution

  • Many months later... I answer my own question in case some remarks could be made about the solution I decided to use.

    As a first attempt, and as kindly suggested by @cdhowie, I started by isolating the data members (only pos here) of Entity in an EntityState structure used as the only data-member of Entity. This way, I could make Animator::animate() expect state: &mut EntityState instead of entity: &mut Entity as parameter; doing so, an implementation of Animator was able to mutate the position of an Entity. However, I was not fully satisfied because this led to a strict distinction between some members of an Entity only because of the borrow-checker. For example, I could not invoke Entity::change_color() from an Animator because it implies the shape member which is not in EntityState. Of course, we could decide to include shape in EntityState as well, but what if we had another behavioural component (Interactor...) able to mutate the Entity (its state) and subject to mutations by other behavioural components (as Animator could want to mutate Shape)? I find it difficult to define a general rule in order to decide which members deserve to stand in EntityState or just in Entity (and using interior-mutability for every single member looks cumbersome to me).

    By chance, while struggling with callback problems (which are quite similar to this problem, actually), I found this answer which uses a trick that I find brilliant and obvious once someone else than me has invented it! The behavioural member that needs &mut self when invoked is stored in an Option. It is simply taken from the Option before the invocation and put back into afterwards: this way a &mut Entity parameter of this invocation cannot reach it any more via the Entity and the borrow-checker finds this situation correct. This solution only requires minimal changes in the original organisation of the code and, as far as I can foresee, it seems to keep being usable when the scenario gets more complex (more behavioural components, eventually interacting).

    Back to the example provided in the question, only three minor changes are needed (playground). In the structure, the member is wrapped into an Option.

        pub struct Entity {
            pos: Pos,
            shape: Box<dyn Shape>,
            // animator: Box<dyn Animator>, // an option is required
            animator: Option<Box<dyn Animator>>,
        }
    

    Obviously, the construction of such a structure considers this Option.

        impl Entity {
            pub fn new(
                pos: Pos,
                shape: Box<dyn Shape>,
                animator: Box<dyn Animator>,
            ) -> Self {
                Self {
                    pos,
                    shape,
                    // animator, // make use of the Option
                    animator: Some(animator),
                }
            }
    // ...
    

    The main point stands here: take the behavioural member from the Option, invoke its function, providing a reference to the whole structure, put back the behavioural member into the Option.

    // ...
            pub fn animate(&mut self) {
                // let anim = &mut self.animator; // take-from/put-back
                // anim.animate(self);
                if let Some(mut anim) = self.animator.take() {
                    anim.animate(self);
                    self.animator = Some(anim);
                }
            }
        }