1

I know why Rust doesn't like my code. However, I don't know what would be the idiomatic Rust approach to the problem.

I'm a C# programmer, and while I feel I understand Rust's system, I think my "old" approach to some problems don't work in Rust at all.

This code reproduces the problem I'm having, and it probably doesn't look like idiomatic Rust (or maybe it doesn't even look good in C# as well):

//a "global" container for the elements and some extra data
struct Container {
    elements: Vec<Element>,
    global_contextual_data: i32,
    //... more contextual data fields
}

impl Container {
   //this just calculates whatever I need based on the contextual data
   fn calculate_contextual_data(&self) -> i32 {
       //This function will end up using the elements vector and the other fields as well, 
       //and will do some wacky maths with it. 
       //That's why I currently have the elements stored in the container
   }
}

struct Element {
    element_data: i32,
    //other fields
}

impl Element {
    //I need to take a mutable reference to update element_data, 
    //and a reference to the container to calculate something that needs 
    //this global contextual data... including the other elements, as previously stated
    fn update_element_data(&mut self, some_data: i32, container: &Container) {
        self.element_data *= some_data + container.calculate_contextual_data() //do whatever maths I need
    }
}


fn main(){

    //let it be mutable so I can assign the elements later
    let mut container = Container {
        elements: vec![],
        global_contextual_data: 1
    };

    //build a vector of elements
    let elements = vec![
        Element {
            element_data: 5
        },
        Element {
            element_data: 7
        }
    ];

    //this works
    container.elements = elements;

    //and this works, but container is now borrowed as mutable
    for elem in container.elements.iter_mut() {
        elem.element_data += 1; //and while this works
        let some_data = 2;

        //i can't borrow it as immutable here and pass to the other function
        elem.update_element_data(some_data, &container); 
    }
}

I understand why elem.update_element_data(some_data, &container); won't work: I'm already borrowing it as mutable when I call iter_mut. Maybe each element should have a reference to the container? But then wouldn't I have more opportunities to break at borrow-checking?

I don't think it's possible to bring my old approach to this new system. Maybe I need to rewrite the whole thing. Can someone point me to the right direction? I've just started programming in Rust, and while the ownership system is making some sort of sense to me, the code I should write "around" it is still not that clear.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Ricardo Pieper
  • 2,613
  • 3
  • 26
  • 40
  • Rust favors DAG dependencies, but here you have a cycle between `Container` and `Element` (they both know about each other), which is where your problems start. In general, Rust frowns at "dodgy logic". For example, in your case, do you obtain in C# the same result if you modify the elements by iterating forward and backward? Is it intended that the order of iteration may modify the result of this update? – Matthieu M. Jun 17 '17 at 17:41
  • If I understood it correctly, yes. It's entirely possible, and expected, that the order in which those elements are updated might influence the final result. The goal, in the end (which is a long way from now) is to find out which order of operations yield the best result. When doing the update, I need at some point check the state of the other elements. You did give me an idea, though: I'll try and first do it in C# properly, and then rewrite it in rust. The problem itself, aside Rust, is also completely new to me. – Ricardo Pieper Jun 17 '17 at 17:53
  • Ah, that's too bad, because if the problem requires being independent of iteration order, it's likely that you'll need two vectors (one with the current state, one being updated) which would neatly solve your issues here ;) – Matthieu M. Jun 17 '17 at 17:55
  • So I'll need to make some copies, then? – Ricardo Pieper Jun 17 '17 at 17:56
  • My idea was that if you could (1) copy the current vector, then (2) iterate mutably over the copy while using the container, then it would work like a charm. It all depends whether this fits your problem or not... – Matthieu M. Jun 17 '17 at 17:59
  • Could you please take some time and write, *in prose*, what the code is supposed to do? 4 of your 5 paragraphs of text consist mostly of self-flagellation and don't really help anyone understand the problem. Having a clear, concise description of the problem you are trying to solve would go a long way! – Shepmaster Jun 18 '17 at 14:34

2 Answers2

0

I came across this question: What's the Rust way to modify a structure within nested loops? which gave me insight into my problem.

I revisited the problem and boiled the problem down to the sharing of the vector by borrowing for writes and reads at the same time. This is just forbidden by Rust. I don't want to circumvent the borrow checker using unsafe. I was wondering, though, how much data should I copy?

My Element, which in reality is the entity of a game (I'm simulating a clicker game) has both mutable and immutable properties, which I broke apart.

struct Entity {
    type: EntityType,
    starting_price: f64, 
    ...
    ...
    status: Cell<EntityStatus>
}

Every time I need to change the status of an entity, I need to call get and set methods on the status field. EntityStatus derives Clone, Copy.

I could even put the fields directly on the struct and have them all be Cells but then it would be cumbersome to work with them (lots of calls to get and set), so I went for the more aesthetically pleasant approach.

By allowing myself to copy the status, edit and set it back, I could borrow the array immutably twice (.iter() instead of .iter_mut()).

I was afraid that the performance would be bad due to the copying, but in reality it was pretty good once I compiled with opt-level=3. If it gets problematic, I might change the fields to be Cells or come up with another approach.

Ricardo Pieper
  • 2,613
  • 3
  • 26
  • 40
  • Rust style is `snake_case` for variables and field names. *compiled with opt-level=3* — It's easier to just do `cargo build --release`. – Shepmaster Dec 04 '17 at 13:57
  • Thanks for the tip. Yeah the actual code follows snake_case, I typed it off the top of my head. As for the `cargo build --release`, does it run on opt-level=3? I just checked and it seems that there are many nuances surrounding --release and the different levels of optimization (https://internals.rust-lang.org/t/default-opt-level-for-release-builds/4581) – Ricardo Pieper Dec 04 '17 at 17:21
0

Just do the computation outside:


#[derive(Debug)]
struct Container {
    elements: Vec<Element>
}

impl Container {
    fn compute(&self) -> i32 {
        return 42;
    }
    fn len(&self) -> usize {
        return self.elements.len();
    }
    fn at_mut(&mut self, index: usize) -> &mut Element {
        return &mut self.elements[index];
    }
}

#[derive(Debug)]
struct Element {
    data: i32
}

impl Element {
    fn update(&mut self, data: i32, computed_data: i32) {
        self.data *= data + computed_data;
    }
}

fn main() {
    let mut container = Container {
        elements: vec![Element {data: 1}, Element {data: 3}]
    };
    
    println!("{:?}", container);
    
    for i in 0..container.len() {
        let computed_data = container.compute();
        container.at_mut(i).update(2, computed_data);
    }

    println!("{:?}", container);
}

Another option is to add an update_element to your container:


#[derive(Debug)]
struct Container {
    elements: Vec<Element>
}

impl Container {
    fn compute(&self) -> i32 {
        let sum = self.elements.iter().map(|e| {e.data}).reduce(|a, b| {a + b});
        return sum.unwrap_or(0);
    }
    fn len(&self) -> usize {
        return self.elements.len();
    }
    fn at_mut(&mut self, index: usize) -> &mut Element {
        return &mut self.elements[index];
    }
    fn update_element(&mut self, index: usize, data: i32) {
        let computed_data = self.compute();
        self.at_mut(index).update(data, computed_data);
    }
}

#[derive(Debug)]
struct Element {
    data: i32
}

impl Element {
    fn update(&mut self, data: i32, computed_data: i32) {
        self.data *= data + computed_data;
    }
}

fn main() {
    let mut container = Container {
        elements: vec![Element {data: 1}, Element {data: 3}]
    };
    
    println!("{:?}", container);
    
    for i in 0..container.len() {
        let computed_data = container.compute();
        container.at_mut(i).update(2, computed_data);
    }

    println!("{:?}", container);
    
    for i in 0..container.len() {
        container.update_element(i, 2);
    }

    println!("{:?}", container);
}

Try it!

Josu Goñi
  • 1,178
  • 1
  • 10
  • 26