Inner mutability and complex trait objects


#1

I’m trying to figure out the best way to implement the code below. The Source and Sink traits are something like a database. The Source allows data to be read from it, and the Sink allows data to be written. A group of writes needs to be finished with a flush in order for the changes to be committed (it’s like a transaction in a database).

The first module is the code I have now. There is an outer type that has some operations. It makes use of an inner type that assists with the write. The write operation will need to be done in an interleaved manner between them (hence complicating the mutable borrow). This first module solves this by not including the Sink in the type, and instead passing it around as necessary. This works, and in some ways maybe the most simple. But, it does make for a lot of data to pass around.

The second module (which I haven’t been able to get to work), was my attempt to put the Sink inside of the type, using Rc and RefCell to allow inner mutability. I’ve not been able to come up with a type for into_inner() that will describe the lifetimes right.

My main question: what would be the recommended way of implementing this? In the real application, these types will have many more methods, and especially Outer will have other Inner types of types that will also have methods that need to be interleaved.

The other way I can think of doing this would be to get rid of the mut on add within Sink. I would have to wrap the struct around another type using Rc or Arc in order to be able to share this item.

Thanks,
David

// Mutability exercise.

#![allow(dead_code)]

fn main() {
    let mut aa = MySource::new();
    {
        let mut aaw = aa.get_sink().unwrap();
        let ff = first::Outer::new();
        ff.frob(&mut *aaw);
        aaw.flush();
    }
    println!("aa: {:?}", aa.all());

    let mut bb = MySource::new();
    {
        let mut bbw = bb.get_sink().unwrap();
        let ss = second::Outer::new(bbw);
        ss.into_inner().flush();
    }
    println!("bb: {:?}", bb.all());
}

// A source is initially opened, the type being determined by what is actually
// found on the device.
pub trait Source {
    fn get_sink<'a>(&'a mut self) -> Option<Box<Sink + 'a>>;
    fn all(&self) -> Vec<u32>;
}

// To update, request a sink, `add` with it, and then flush.
pub trait Sink {
    fn add(&mut self, item: u32);
    fn flush(self: Box<Self>);
}

// A very simple implementation of this.
struct MySource {
    items: Vec<u32>,
}

impl MySource {
    fn new() -> MySource {
        MySource {
            items: Vec::new(),
        }
    }
}

impl Source for MySource {
    fn all(&self) -> Vec<u32> {
        self.items.iter().map(|&x| x).collect()
    }

    fn get_sink<'a>(&'a mut self) -> Option<Box<Sink + 'a>> {
        Some(Box::new(MySink {
            parent: self,
        }))
    }
}

struct MySink<'a> {
    parent: &'a mut MySource,
}

impl<'a> Sink for MySink<'a> {
    fn add(&mut self, item: u32) {
        self.parent.items.push(item);
    }

    fn flush(self: Box<Self>) {
        // Nothing to do in the example.
    }
}

//----------------------------------------------------------------------
// For example.

// In this first instance, the two structs don't contain the Sink and it gets
// passed around as needed.  This works, but the Sink has to be passed all
// around to make things work.
mod first {
    use super::*;

    pub struct Outer {
        other: u32,
    }

    impl Outer {
        pub fn new() -> Outer {
            Outer {
                other: 1,
            }
        }

        pub fn frob(&self, sink: &mut Sink) {
            let inside = Inner::new();
            sink.add(self.other);
            inside.frob(sink, self.other + 1);
            sink.add(self.other + 2);
        }
    }

    struct Inner {
        other: u32,
    }

    impl Inner {
        fn new() -> Inner {
            Inner {
                other: 100,
            }
        }

        fn frob(&self, sink: &mut Sink, extra: u32) {
            sink.add(self.other + extra);
        }
    }
}

// For this second instance, we wrap up the Sink with inner mutability so that
// it can just be stored directly in the instances.
mod second {
    use super::*;
    use std::cell::RefCell;
    use std::rc::Rc;

    pub struct Outer<'a> {
        other: u32,
        sink: Rc<RefCell<Box<Sink + 'a>>>,
    }

    impl<'a> Outer<'a> {
        pub fn new<'b>(sink: Box<Sink + 'b>) -> Outer<'b> {
            Outer {
                other: 1,
                sink: Rc::new(RefCell::new(sink)),
            }
        }

        pub fn into_inner(self) -> Box<Sink + 'a> {
            // cannot move out of borrowed content
            self.sink.into_inner()
        }
    }
}

#2

I haven’t attempted to wrap my head around the purpose of the code, but Outer::into_inner must fail because Outer only has a Rc shared reference to the trait object, not a unique reference. If it’s meant to be unique, then get rid of the Rc and the method will work (you don’t need it for interior mutability); if it’s meant to be shared, you’ll need to do something like clone the Rc, return a reference, etc.


#3

This may be a late reply, but when the topic was fresh, my understanding of Rust was still much shakier than today :smile:

Your original implementation of into_inner() would have consumed the RefCell and tried to move it out of Outer, which the compiler disallowed. According to my understanding of RefCell, trying to return the enclosed mutable reference (obtained with borrow_mut()) outside of the scope which requested the reference would also fail. For a possible solution, see this gist (sorry about uneven tabs.) It doesn’t propagate the sink out of the Outer struct, but offers its use via use_sink() and closures. True, it seems that not much has changed when you have to propagate a reference to Outer to all Inner methods, but I think it’s a cleaner design if Outer were to offer more resources.

The gist should be compiled with --cfg second to produce the second implementation variant.