Extending object traits to an arena type data structure

Hi!

I'm trying to write an application that manipulates visual objects such as circle, rectangle, etc.

Naturally, I've written traits such as the one below to group functionalities and implemented them across multiple shapes

pub trait SetPosition{
  fn move_by(&mut self, x:f32, y:f32); //shift object's position
  fn move_to(&mut self, x:f32, y:f32); // move to a specific point
}

I'm now exploring the use of arena allocator (e.g. generational-arena) to contain all object data and use only index to operate on the object data (due to reference cycles, etc.)

The problem I have is with implementing these traits for the index type I get from arena allocation. I would like to stay as close to my original trait definition as possible, but now I have to pass around the arena for every method:

let mut c = circle();
let mut arena = Arena::new();
let mut c_idx = arena.add(c); // Add object and return it's index

c.move_by(1,1); // This is previous method 
c_idx.move_by(&mut arena, 1, 1) // Now arena must also be passed as an input

Also, this change requires that I define a new trait for every existing trait due to the change in each function signature. I find this cumbersome and hard to maintain.

Is there a more ergonomic design pattern which I could use?

Thank you!

I don't think you should implement anything on the index type. Instead you should always use the arena to retrieve the objects:

fn move_circle(arena: &mut Arena<Circle>, c_idx: Idx<Circle>, x: f32, y: f32) {
    let c = &mut arena[c_idx];
    c.move_by(x, y); // no additional trait needed !
}

You still have to pass arena around, but that's unavoidable when using Arenas I suppose :slightly_smiling_face:

Thanks @arnaudgolfouse for your comment!

I think I understand your point. So is it normally recommended to have all methods that access object data to have arena as an input and index as another input, plus any method-specific arguments?

Also, in this scheme of having a single mutable arena passed around everywhere, does it pose any risk in terms of accidentally mutating any other objects?

By using indirect reference (i.e. index) to an object, I can safely and freely distribute copies of the reference, but it also allows any consumer of any object data to have mutable access to all of data.

In which kind of applications is it appropriate to consider using the arena allocation?

Ah I see other people basically were saying the opposite of me... Uh they may be more knowledgeable than me in this regard :sweat_smile: Let's try to come up with something good :

Based on the thread above I assume your application looks something like

/// Implements `move_by`
pub struct Circle {
    position: (f32, f32),
    children: Vec<Index> // these should move when I move !
}

pub struct Application {
    arena: Arena<Circle>,
}

In some way, yes. In your use-case I guess it is unavoidable, because when an object moves it usually takes other objects with it (I assume), so it needs some sort of context to refer to.
Since you are using Arena indexes, these references (Index) only means something if you give them the Arena in question.

But you could also introduce methods that gives back a reference to Circle:

impl Application {
    pub fn get_circle(&self, idx: Index) -> &Circle {
        &self.arena[idx]
    }

    pub fn get_circle_mut(&mut self, idx: Index) -> &mut Circle {
        &mut self.arena[idx]
    }
}

And then you can call whatever you want on these references :slightly_smiling_face:

I think you should not directly expose the Arena. Instead, treat Application as the global context you need to refer to:

impl Application {
    pub fn add_circle(&mut self) -> Index {
        self.arena.insert(Circle::new())
    }

    pub fn get_circle_position(&self, c_idx: Index) -> (f32, f32) {
        self.arena[c.idx].position
    }

    pub fn circle_move_by(&mut self, c_idx: Index, x: f32, y: f32) {
        let c = &mut self.arena[c_idx];
        c.move_by(x, y);
        for child in c.children {
            self.circle_move_by(child, x, y)
        }
    }
}

fn main() {
    let mut application: Application = /* ... */;
    let c = application.add_circle();
    application.circle_move_by(c, 1.0, 1.0);
}

So all modifications are properly encapsulated :slightly_smiling_face:

From my experience:

  • In graph or tree-like application, kinda like the one with Circles.
  • In any application which typically requires self-referential data.
    For example, if you have this:
    struct Application<'a> {
        circles: Vec<Circle>,
        active_circles: Vec<&'a Circle>
    }
    
    Rust will refuse to let active_circles refer to circles, for (justified !) lifetimes reasons. So you can use Arena to circumvent this:
    struct Application {
        circles: Arena<Circle>,
        active_circles: Vec<Index>
    }
    

This typically arise in most gui or game applications, but really it can appear anywhere.

@arnaudgolfouse Thank you very much for your thorough answer!

Yeah it seems like my code will end up looking that way. It's taking me some time to get comfortable with the mindset of having all method calls centred around the application, instead of treating each object as free-standing variable and having methods around it.

Thanks for the example above! My main interest is in gui or graphics applicaiton, so I guess I'll have to learn to be very comfortable with this type of data-structure:)

Thank you for taking the time to think this through with me. Your comment has been very helpful!

1 Like