Subtypes using composition + traits

I want to have a Node type that stores a reference to the parent node, a transform property, rotation degrees, opacity, signals, among other things. I want to have cheap access to these properties, and want to implement them in Node, not in the Node subtypes themselves.

Approach 1

Well, the approach to implement a inheritance of nodes with these Node common properties like position and opacity is using a Node struct containing a "subtype" property. Kind of like:

struct Node {
    m_parent: Option<Weak<Node>>,
    m_subtype: Arc<dyn NodeSubtype>,
}

trait NodeSubtype {
}

Here's what I sketched about subtypes a few days ago (slightly edited):

Subtype Trait

In regards to node subtypes, the Node trait implements:

  • node.to::<T>(): allows to access subtype properties and methods. It returns Arc<T>.
  • node.is::<T>()

Use this to retrieve TypeId from a type parameter.

use core::any::TypeId;
TypeId::of::<T>();

The trait NodeSubtype implements:

  • fn draw(&self, node: &Arc<Node>) {}
  • fn process(&self, node: &Arc<Node>) {}
  • fn ready(&self, node: &Arc<Node>) {}

As you can see, this allows NodeSubtype to access the respective Node properties in certain events like in draw(). But with still that, NodeSubtype doesn't always have access to its respective Node. So, for example, if a NodeSubtype has a method f(), that method f() can only access properties and methods defined by NodeSubtype.

Is that a good approach to inheritance?

Approach 2

I could define Node as a trait and provide a single struct that covers all the common properties of nodes. I can then provide aliases for properties in this common struct, such as fn x(self: &Arc<Self>) -> f64 { self.common().x }

use std::sync::{Arc, Weak};

struct NodeCommon {
}

trait Node {
	fn common(self: &Arc<Self>) -> Arc<NodeCommon>;
}

struct TabBar {
	m_common: Arc<NodeCommon>,
}

impl Node for TabBar {
	fn common(self: &Arc<Self>) -> Arc<NodeCommon> {
		self.m_common.clone()
	}
}

What is better?

This reminds me of Passing Any payload in events, but not sue if the the problem is related. Anyway, I'm curious how to solve these kinds of problems.

Perhaps it could help to look at how these would be (idiomatically) solved without Arcs, and then decide if the same approach can be made to work with Arc?

You could try something along these lines as well:

use std::any::Any;
use std::sync::{Arc,Weak};

struct Node<Extension = ()> {
    parent: Weak<dyn AnyNode>,
    // other common fields here
    extension: Extension
}

trait AnyNode: AsAny {
    // Methods that can be applied to any node type
}

impl<T:NodeExtension> AnyNode for Node<T> {
    // ...
}

impl<T:NodeExtension> Node<T> {
    fn build(parent: Weak<dyn AnyNode>, mut extension:T)->Arc<Self> {
        Arc::new_cyclic(|node| {
            extension.register_node(node.clone());
            Node {
                parent, extension
            }
        })
    }
}

trait NodeExtension: 'static + Send + Sync + Sized {
    // Subtype methods needed to support methods defined in AnyNode
    fn register_node(&mut self, node: Weak<Node<Self>>);
}

impl dyn AnyNode {
    pub fn downcast_ref<T:NodeExtension>(&self)->Option<&Node<T>> {
        self.as_any().downcast_ref()
    }

    pub fn downcast_mut<T:NodeExtension>(&mut self)->Option<&mut Node<T>> {
        self.as_any_mut().downcast_mut()
    }

    pub fn downcast_arc<T:NodeExtension>(self: Arc<Self>)->Option<Arc<Node<T>>> {
        Arc::downcast(self.as_any_arc()).ok()
    }
}

trait AsAny: 'static + Send + Sync {
    fn as_any(&self)->&(dyn Any + Send + Sync);
    fn as_any_mut(&mut self)->&mut (dyn Any + Send + Sync);
    fn as_any_arc(self: Arc<Self>)->Arc<(dyn Any + Send + Sync)>;
}

impl<T:'static + Send + Sync> AsAny for T {
    fn as_any(&self)->&(dyn Any + Send + Sync) { self }
    fn as_any_mut(&mut self)->&mut (dyn Any + Send + Sync) { self }
    fn as_any_arc(self: Arc<Self>)->Arc<(dyn Any + Send + Sync)> { self }
}
1 Like

@hydroper1 Note the Weak, which is probably pretty important to avoid memory leaks. I would assume that normally the parent keeps a strong reference to its children, and each child only a weak reference to its parent.

1 Like

Yeah, I forgot to change that in the original approach examples.

@2e71828 this approach seems a bit complex, but interesting.

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.