Feedback on attempt to design a system with parent/child relationship

I am messing around building a system where the idea is Context would build and store a singleton ChildContext. The Context and ChildContext itself would be able to clear the ChildContext by removing the parent Context from the ChildContext. Then the ChildContext will become defunct by checking for parent. Although there is only one ChildContext in this example, the idea is there will be specialised contexts but will have the same base code handling the contexts' relationships

In my head the natural thing to do would be Context would own the ChildContext and the ChildContext would have a weak reference to the Context. ChildContext could then call on Context to clear it self up. My attempt at rustifying is below. It uses interior mutability to achieve the idea.

My questions are:

  1. I am not sure if this the right way to handle it
  2. With the specialised contexts and being able to release from the child and the parent, is there a better way to avoid code duplication?

Updated code

use std::cell::RefCell;
use std::mem;
use std::rc::{Rc, Weak};


struct Context {
    internal: Rc<RefCell<ContextInt>>
}

struct ContextInt {
    child_context: Option<Rc<RefCell<ChildContextInt>>>
    // ...
    // other child contexts
}

struct ChildContext {
    internal: Rc<RefCell<ChildContextInt>>
}

struct ChildContextInt {
    parent_context: Option<Weak<RefCell<ContextInt>>>
    // ...
    // context data
}

impl Context {
    pub fn new() -> Self {
        Context {
            internal: Rc::new(RefCell::new(ContextInt {
                child_context: None
            }))
        }
    }

    pub fn child_context(&self) -> ChildContext {
        let internal = self.internal.borrow_mut();
        
        let child_context_int: Rc<RefCell<ChildContextInt>>;
        if let Some(child_context_ref) = &internal.child_context {
            child_context_int = Rc::clone(&child_context_ref);
        } else {
            child_context_int = Rc::new(
                RefCell::new(
                    ChildContextInt {
                        parent_context: Some(Rc::downgrade(&self.internal))
                    }
                )
            );

            self.internal.borrow_mut().child_context = Some(Rc::clone(&child_context_int));
        }
    
        ChildContext { 
            internal: child_context_int 
        }
    }

    pub fn release_child_context(&self) {
        let mut child_context: Option<Rc<RefCell<ChildContextInt>>> = None;
        mem::swap(&mut child_context, &mut self.internal.borrow_mut().child_context);

        if let Some(child_context) = child_context {
            child_context.borrow_mut().parent_context = None
        }
    }
}

impl ChildContext {
    fn release(&self) {
        let mut parent_context: Option<Weak<RefCell<ContextInt>>> = None;
        mem::swap(&mut parent_context, &mut self.internal.borrow_mut().parent_context);

        if let Some(parent_context_rc) = parent_context {
            if let Some(parent_context) = parent_context_rc.upgrade() {
                parent_context.borrow_mut().child_context = None
            }
        }
    }
}

Well then. That's a lot of Rc<RefCell<...>>'s. Like, a lot of a lot; and I'm not even sure what exactly are you trying to accomplish, even after reading your post several times over.

  • are you trying to port some Java/C#/some other GC'ed class to Rust? (bad idea)
  • what do you expect this Context to contain? (other than a Rc<RefCell<...>> of itself)
  • are you sure you know what a singleton is? (as the "single" part is a bit missing here)
  • how are you planning to use these (Int)(Child)Context's? (and why so many wrappers?)

Not sure anyone will be able to advise you here, without any sense of the spec/requirements you have in mind, as a whole. Share a bit more about what you're trying to achieve, perhaps?

1 Like

I have a couple questions before trying to suggest a data structure.

  1. I assume there are values stored in the child contexts and you haven't shown them. Are these always integers or should they be generic?
  2. Are those values also stored in the context parents, or only in the child?
  3. What type of sharing do you need among contexts?:
    • Do you need to share child contexts among parents (a child may appear in more than one parent)?
    • Or do you need to share values (integers or a generic type) among child and/or parent contexts?
    • Or do you not need any sharing?
  4. Are there only two levels in the hierarchy (parent and child) or are there N levels?

I am just architecting an idea.

Exactly. I fell back on old habits and looking for a more rust way of doing it.

Nothing much. It will be mostly used to create specific ChildContexts and store them internally, making sure to only create one.

Yes. Context::child_context should only create one ChildContext. I see the confusion as there is no logic currently to check if one has been created yet. The Context's internals will have multiple specific child contexts, but for now there is only one.

Let me answer the bracket question first: the wrappers are what will be exposed or publicised. Context and ChildContext are the public structs. The internals are there to be mutated, allowing releasing the ChildContext and affectively setting it to null or None. The internals of ChildContext would then have its parent set to None If a method was called on the ChildContext, if it doesn't have a parent, it would just return and do no actions.

I will update the code.

I have no plans to using generics. Any data attached should be concrete types.

Only the child

There will only be one parent. A parent will only have many individual children. No information will be shared between the children.

Only two levels.

I'm not sure I can suggest a better data structure without knowing more about how it is used. But I can at least suggest a minor improvement:

    pub fn release_child_context(&self) {
        // let mut child_context: Option<Rc<RefCell<ChildContextInt>>> = None;
        // mem::swap(
        //     &mut child_context,
        //     &mut self.internal.borrow_mut().child_context,
        // );

        let child_context = self.internal.borrow_mut().child_context.take();
impl ChildContext {
    fn release(&self) {
        // let mut parent_context: Option<Weak<RefCell<ContextInt>>> = None;
        // mem::swap(
        //     &mut parent_context,
        //     &mut self.internal.borrow_mut().parent_context,
        // );

        let parent_context = self.internal.borrow_mut().parent_context.take();

Sure, I can imagine a generic implementation of a child context that can be reused for different context fields. It must be generic because the data element contained in the child context may have a different type for each parent context field (I assume).

One question first: When you clear a child context, is it sufficient to drop the contained data element? Or do you also need to deallocate its Rc as you're currently doing, presumably to reduce memory usage? The former is simpler.

Assuming the latter, here is one way to do it: playground (minor update made)

The real question is why you need the Rc for each context.

Normally a simple reference would be returned to the context data. Did you run into lifetime issues when doing that? If so and you think that was related to trying to program in Rust as you would in a language with GC, then you may want to try it without Rc and see if people here can help get that working.

If you're sure you need Rc, then just ignore this.

This sounds very peculiar to me, because singletons are not stored in objects and are not build multiple times.

A singleton is, when you have in the whole program only exactly one instance of an object. Very typically the constructor of a singleton is private, and instead there is some global method or function to access this one/same instance.

Additionally, if you would work with singletons in Rust, they need to be thread safe, so Rc does not work because it is neither Send nor Sync. You have to use Arc instead.

However, your code (signature):

impl Context {
    pub fn child_context(&self) -> ChildContext {
        ...
    }
}

With your comments:

And:

My interpretation is, you are trying to build something different:

I will guess and speculate, that ChildContext should be some sort of proxy, that provides some (sub) functionality of Context. The additional option of this proxy should be to enable termination of the functionality at any time, either by ChildContext itself or by the parent Context object.

This can be done much more simply, even using Rc when not needed to be thread safe. For example:

use std::cell::RefCell;
use std::rc::Rc;

type CtxInnerRef = Rc<RefCell<ContextInner>>;

struct Context(CtxInnerRef);

struct ContextInner {
    id: u64,
    // other data
}

struct ChildContext {
    id: u64,
    ctx_inner: CtxInnerRef,
}

impl Context {
    pub fn new() -> Self {
        let inner = ContextInner {
            id: 1,
        };
        Self( Rc::new(RefCell::new(inner)) )
    }
    
    pub fn child_context(&self) -> ChildContext {
        let id = self.0.borrow().id;
        let ctx_inner = Rc::clone(&self.0);
        ChildContext { id, ctx_inner }
    }
    
    pub fn release_child_context(&self) {
        let mut inner = self.0.borrow_mut();
        inner.id += 1;
    }
}

impl ChildContext {
    pub fn release(&mut self) {
        self.id = 0;
    }
    
    pub fn some_function(&self) {
        let ctx_inner = self.ctx_inner.borrow(); // or borrow_mut()
        if self.id == ctx_inner.id {
            // ctx_inner.some_function()
        }
    }
}

Of course, there a unlimited variants of this theme (Weak, bool, ...).

Feel free to tell what are you really trying to solve, not what solution do you think should work.

1 Like

Looking over all your replies, it made me realise a few things

  • Context is the owner of the internals of ChildContext, making the relationship, Context should know about ChildContext but ChildContext doesn't need to know about Context. Before the only reason Context was there is so to link back is to release the ChildContext from Context.
  • I want something that will enforce that if the internals of ChildContext is released that any further calls to it become defunct

With these, I coded something new below. My types are unwieldy, but to me it makes sense to me. Context is the owner of all ChildContexts, but each ChildContext is able to release themselves if they are no longer needed. Because of Rc<RefCell<...>>, Context will be updated by ChildContext::release(&self) (It might be worth switching Rc for Arc).

My ideas are built around the architecture rather than hard data usage. This may come back to bite me, but I enjoy playing around with ideas. I liked some of the ideas jumpnbrownweasel came up with in their playground. Initially, instead of using generics I was thinking about somehow using traits – for instance, putting common behaviour like release – but I am not sure now how viable that would be.

Update: Fixed some bugs

use std::cell::RefCell;
use std::mem;
use std::rc::{Rc, Weak};

struct Context {
    internal: Rc<RefCell<ContextInternal>>
}

struct ContextInternal {
    child_context: Option<Rc<RefCell<ChildContextInternalPtr>>>
    // ...
    // other child contexts
}

struct ChildContext {
    internal: Rc<RefCell<ChildContextInternalPtr>>,
}

type ChildContextInternalPtr = Option<ChildContextInternal>;

struct ChildContextInternal {
    // ...
    // data
}

impl Context {
    fn new() -> Self {
        Context {
            internal: Rc::new(RefCell::new(ContextInternal {
                child_context: None
            }))
        }
    }

    fn child_context(&self) -> ChildContext {
        let mut internal = self.internal.borrow_mut();
        
        let child_context_int: Rc<RefCell<ChildContextInternalPtr>>;
        if let Some(child_context_ref) = &internal.child_context {
            child_context_int = Rc::clone(child_context_ref);
        } else {
            child_context_int = Rc::new(
                RefCell::new(
                    Some(ChildContextInternal {
                        
                    })
                )
            );

            internal.child_context = Some(Rc::clone(&child_context_int));
        }
    
        ChildContext { 
            internal: child_context_int,
        }
    }

    fn release_child_context(&self) {
        let mut internal = self.internal.borrow_mut();
        // set ptr to null
        let child_context = internal.child_context.take();
        if let Some(child_context) = child_context {
            *child_context.borrow_mut() = None;
        }
    }
}

impl ChildContext {
    fn release(&self) {
        self.internal.borrow_mut().take();
    }

    fn do_something(&self) -> bool {
        let internal = self.internal.borrow();

        if let Some(internal) = &*internal {
            // do something
            // ...
            true
        } else {
            false
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn child_context_release() {
        let context = Context::new();

        let child_context = context.child_context();
        assert!(child_context.do_something());

        child_context.release();

        assert!(! child_context.do_something());
    }

    #[test]
    fn context_release() {
        let context = Context::new();

        let child_context = context.child_context();
        assert!(child_context.do_something());

        context.release_child_context();

        assert!(! child_context.do_something());
    }
}

If you use traits, normally you will be using generics. The generic type will have a trait bound.

The exception is when you use a dyn Trait (erased type). In that case you don't use a generic type.

I mean I could argue the other way too. From Wikipedia,

In object-oriented programming, the singleton pattern is a software design pattern that restricts the instantiation of a class to a singular instance.

I am restricting the instantiation of ChildContexts to a singular instance controlled by Context, just not in the global space. I would argue a singleton does not have to be in the global space, it is just often used that way.

With my lasted code example, I have come to the same conclusion.

You are correct in your interpretation.

Unfortunately, that's the standard way to do it.

Parent -> Child is an Rc, and Child -> Parent is Rc::Weak. This is more general than you really want, because everything is single-ownership. But that's what we have to work with. You don't have to worry about deallocation; that's automatic. You don't have to worry about circular references; Rc::Weak can't create that problem. So you can't leak memory this way.

You have to use RefCell and borrow to access anything, because the compile time borrow checker doesn't know how to analyze this sort of thing. So the checking is punted to run-time in borrow.

There's no simple, elegant, safe solution. But there's a verbose safe solution, as outlined above.

Theory:

I've suggested ways to have Rust single ownership with weak back references, checked at compile time. The general idea is to prove at compile time that the borrow calls on a data item don't overlap.
This is possible with a conservative check and local borrows only. But such a check is a mis-match to the way the compiler currently does global analysis, which is done before template expansion.

It's similar to deadlock analysis for Mutex, and if we ever get that, we might get borrow conflict analysis too.

PhD-sized topic.

Which is precisely why the first portion of code looked a little odd to me. Make it:

Summary
type Local<T> = Rc<RefCell<T>>;
type LocalParent<T> = Weak<RefCell<T>>;
// as opposed to `Shared<T> = Arc<Mutex<<T>>;`

struct Context(Local<ContextInner>>);

struct ContextInner {
    child: Option<Local<ChildContext>>
}

struct ChildContext {
    parent: LocalParent<ContextInner>
}

not an internal: Rc<RefCell<ContextInt>> of Context wrapping a child_context: Option<Rc<RefCell<ChildContextInt>>> of a ContextInt referencing a parent_context: Option<Weak<RefCell<ContextInt>>> of a ChildContextInt stored inside of a internal: Rc<RefCell<ChildContextInt>>of a ChildContext itself. That's just plain silly.

Unless you're opting out for a different language entirely, agreed. If you absolutely must rely on circular references, however, opting out of manual memory management in favour of a GC isn't the craziest thing to do. Not until there comes along an impl<T> AutoClone for Local(Parent)<T> that wouldn't require peppering in .clone() all over the place. That's a whole other topic, though.

But it isn't silly. There can be many copies of ChildContext, but they need to all point to the same thing. That's why ChildContext has internals and why Context owns and points to the internals of ChildContext. In your code example under Summary, I would have Rc<RefCell<ChildContext>> everywhere. To me at least, that is an unnecessary complication for the end developer (me or someone else). It is better that I encapsulate and mange it all under the hood.

Are you saying I should not code it in Rust and opt for a language that is GC (Garbage Collected), because that does sound crazy? Circular references will happen even in Rust, that's why Weak exists. And it may not be possible to opt out. Take the Apple's ecosystem: you can't just opt out of Swift for one that is GC because of circular references, you learn to notice them and code for it.

Looking over the code again, did you mean

struct Context {
    internal: Rc<RefCell<ContextInternal>>
}

should not in fact be Rc? If that is the case, I can see now it doesn't need to be there. It just needs to be the RefCell.

No, I was asking a more general question about how you arrived at this data structure and why you need the sharing. At this point, probably best to just ignore that question.

You do need the Rc in Context if you want to share it with the child context, i.e., if the child needs a pointer to the parent.