Rust design for notifying parent/owner object

Hello, I am new to Rust. I'm writing a program where one object (One) owns another (Two). One regularly calls into Two but occasionally I need Two to notify One that some particular event has occurred internally to Two.

Can someone suggest a design pattern/method to make this a possibility?


struct One {
    count: u32,
    two: Two, 
}

impl One {
    fn new() -> One {
        One {
            count: 0,
            two: Two::new(), 
        }
    }
    
    fn go(&mut self, n: u32) {
        while self.count != n {
            self.two.step(self);
        }
    }
    
    fn step(&mut self) {
        self.count += 1;
    }
}

struct Two {
    count: u32,
}

impl Two {
    fn new() -> Two {
        Two {
            count: 0,
        }
    }
    
    fn step(&mut self, one: &mut One) {
        self.count += 1;
        if (self.count % 10) == 0 {
            one.step();
        }
    }
}

fn main() {
    let mut one = One::new();
    one.go(20);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `self.two` as mutable more than once at a time
  --> src/main.rs:17:13
   |
17 |             self.two.step(self);
   |             ^^^^^^^^^----^----^
   |             |        |    |
   |             |        |    first mutable borrow occurs here
   |             |        first borrow later used by call
   |             second mutable borrow occurs here

error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/main.rs:17:27
   |
17 |             self.two.step(self);
   |             -------- ---- ^^^^ second mutable borrow occurs here
   |             |        |
   |             |        first borrow later used by call
   |             first mutable borrow occurs here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` (bin "playground") due to 2 previous errors

My actual system is this: I have a Device with multiple Components. The components (and Device, actually) implement a Writable trait. So the Device calls component.write() often, and the interface across all devices is the same. Some components need to let the Device know that an event has occurred. For example, component1 needs to tell the device that it should not use component2 in the next time step. How?

Unfortunately Rust doesn't deal well with mutable graphs due to the borrow checker. Is there a way that you could structure your program's dependencies in a tree fashion instead of a graph with cycles? I ran into a lot of this stuff while I was learning rust a few months ago and a lot of those frustrations went away when I realized that Rust programs prefer to be structured in a particular way.

I was going to suggest at first that One could use the "interior mutability" pattern, so that you could pass a reference to one instead of a mutable reference, but given that there is already a mutable reference in One::go when it calls self.two.step means that I don't think that would work either, since you can't have both immutable and mutable references to a value at the same time.

This sounds like an architecture issue where you are taking a pattern that would normally be fine in a language like JavaScript or Python and applying it to Rust. Unfortunately, callback mechanisms like this tend to rely on the pervasive use of shared mutation, and that doesn't fit well with the borrow checker.

I wrote about an almost identical situation, and some proposed solutions, in my "Common Newbie Mistakes and Bad Practices in Rust: Bad Habits" article.

4 Likes

I would like to maintain the hierarchy since it models the real life implementation more accurately.

Instead, I will return an enum with the signal as an enum value. I am not terribly happy with that solution, but it'll do.

Thank you for your post. It does look very much like your article.

I think working with the return-value-as-signal will work for now.

1 Like

Another option would be for Device to set up a mpsc channel for receiving notifications, and then give a copy of the Sender to each child component. You'll need to manually check for new messages periodically, but that's a single check instead of one for each child.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.