0

I have implemented the observer pattern in Rust. In my implementation, observers are just callback functions, and observables are things that can store optional callback functions, and they call them whenever the associated setter is called.

See below:

mod data {
    pub struct Point {
        //private fields, must be accessed through getter and setter
        x: i32,
        on_x_changed: Option<Box<dyn Fn(&i32)>>,
        y: i32,
        on_y_changed: Option<Box<dyn Fn(&i32)>>,
    }
    impl Point {
        pub fn new() -> Point {
            Point {
                x: 0,
                on_x_changed: None,
                y: 0,
                on_y_changed: None
            }
        }
        pub fn x(&self) -> &i32 {
            &self.x
        }
        pub fn set_x(&mut self, x: i32) {
            if x == self.x {return;}
            self.x = x;
            if let Some(f) = &self.on_x_changed {
                f(&self.x);
            }
        }
        pub fn y(&self) -> &i32 {
            &self.y
        }
        pub fn set_y(&mut self, y: i32) {
            if y == self.y {return;}
            self.y = y;
            if let Some(f) = &self.on_y_changed {
                f(&self.y);
            }
        }

        pub fn set_on_x_changed(&mut self, f: Box<dyn Fn(&i32)>) {
            self.on_x_changed = Some(f);
        }
        pub fn set_on_y_changed(&mut self, f: Box<dyn Fn(&i32)>) {
            self.on_y_changed = Some(f);
        }
    }
}

This works for simple examples, but I want to set the on_x_changed callback to a closure that borrows a captured item. This gives an error:

fn main() {

    //observable
    let mut p = (data::Point::new(), data::Point::new());

    //observer
    let on_p0_x_changed = |p0_x: &i32| {
        println!("{}", p0_x);
        println!("{}", p.1.x()); //p.1 is captured
        //check if p0_x collides with p1_x...
    };

    p.0.set_on_x_changed(Box::new(on_p0_x_changed)); // on_p0_x_changed closure lives as long as p, beause p owns it

    // p.1.set_x(1);
    // p.0.set_x(1);
       
}

error[E0597]: `p.1` does not live long enough
  --> src\main.rs:55:33
   |
54 |     let on_p0_x_changed = |p0_x: &i32| {
   |                           ------------ value captured here
55 |         println!("{} {}", p0_x, p.1.x());
   |                                 ^^^ borrowed value does not live long enough
...
59 |     p.0.set_on_x_changed(Box::new(on_p0_x_changed));
   |                          ------------------------- cast requires that `p.1` is borrowed for `'static`
...
65 | }
   | - `p.1` dropped here while still borrowed

If I give the on_x_changed a lifetime annotations, I get a different error.

pub struct Point<'a> {
    //private feilds, must be accessed through getter and setter
    x: i32,
    on_x_changed: Option<Box<dyn Fn(&i32) + 'a>>,
    y: i32,
    on_y_changed: Option<Box<dyn Fn(&i32) + 'a>>,
}
//and also update the Impl with <'a>...

error[E0597]: `p.1` does not live long enough
  --> src\main.rs:58:24
   |
56 |     let on_p0_x_changed = |p0_x: &i32| {
   |                           ------------ value captured here
57 |         println!("{}", p0_x);
58 |         println!("{}", p.1.x()); //p.1 is captured
   |                        ^^^ borrowed value does not live long enough
...
67 | }
   | -
   | |
   | `p.1` dropped here while still borrowed
   | borrow might be used here, when `p` is dropped and runs the destructor for type `(Point<'_>, Point<'_>)`

I'm not super confident with lifetime annotations, but I'm pretty sure all they do is add constraints, and the above example just constrains the callbacks that I can assign to on_x_changed and on_y_changed to have equal lifetimes. Is this correct? Whatever the case, it gives a more readable error message.

It suggests that p.1 might still be used in the on_p0_x_changed closure , when p is dropped at the end of main.

However it is pretty clear that the on_p0_x_changed closure will be dropped at the same time a p, because p owns the on_p0_x_changed closure. So p.1 cannot be used after p is dropped.

Is there a way to help the compiler understand this?

Herohtar
  • 5,347
  • 4
  • 31
  • 41
Blue7
  • 1,750
  • 4
  • 31
  • 55
  • Even if you fix the lifetime issue, you'll have the problem where one of the tuple values contains a reference to another and [that's problematic by ittself](https://stackoverflow.com/q/32300132/501250). For example, just moving the tuple will invalidate the reference stored in the closure. – cdhowie May 25 '22 at 14:44
  • I'm struggling to get my head around that answer. I'll have to read it again later. But as for my tuple, I originally had `p` as two different objects `p0` and `p1`, but then I put them into a tuple in hopes that the compiler would see that they have equal lifetimes. I would happily separate them again if this helps find a solution. – Blue7 May 25 '22 at 14:54
  • Assuming this is going to be part of a larger structure, I don't see much of a way around this problem without something like `Rc`. – cdhowie May 25 '22 at 15:19
  • Yeah, the top answer in https://stackoverflow.com/questions/37572734/how-can-i-implement-the-observer-pattern-in-rust also suggests using an Rc. I would like to understand why this is not a problem in other languages though. Using the observer pattern without reference counting has never been an issue for me before. – Blue7 May 25 '22 at 15:22
  • 1
    Which "other languages?" Many languages do garbage collection automatically and you can't opt out of it, so everything is boxed. It's an apples-to-oranges comparison -- in those languages nearly every value would be like a `Rc>` in Rust, except with cycle detection. If you mean a language like C++, what you're trying to do here would result in undefined behavior due to dangling references, which Rust is not-so-subtly telling you. – cdhowie May 25 '22 at 15:25
  • I meant C++. What I don't understand is how this would result in a dangling reference. – Blue7 May 25 '22 at 15:51
  • As soon as the `Point` referenced in the closure moves, the reference is dangling. For example, if the `Point` is stored in a vector and you insert an item, that can cause the vector to need to grow its internal allocation, and it will move _all_ of its contents to the new location. In your code, `p` is a single value owning both the reference and the `Point`, which is disallowed -- you could move `p` itself (`let p = p;` is sufficient to cause a move) which would invalidate the reference. – cdhowie May 25 '22 at 15:53
  • The lifetime in particular doesn't solve the issue because for `Point<'a>` you're trying to have `'a` refer to the point itself, and a type's lifetime parameter can't refer to its own lifetime, for all of the above reasons. – cdhowie May 25 '22 at 15:55
  • Ah, I think I see. I would not have put the Points in a Vec for that exact reason. But the p owning the reference and the value is the thing that is problematic. – Blue7 May 25 '22 at 16:00
  • Right, a value generally can't own a reference into itself because of the problem with moves (the reference is invalidated as soon as the value is moved). You can resolve that with pinning and a bit of unsafe code to set up the pinned value, but that requires boxing. – cdhowie May 25 '22 at 16:51
  • See the [`pin-project`](https://crates.io/crates/pin-project) crate, super super useful for situations like this. However, honestly I would recommend avoiding pinning if possible since things get really complex really fast. IMO it's almost always worth it to just use `Box` instead and live with the tiny performance hit (when possible). – Coder-256 May 25 '22 at 21:58

0 Answers0