Is it possible to build custom component subtypes?

Let's say you have an abstract node type where you need every subtype to inherit its x and y properties. For example, from a Java-like language:

class Node {
    var x:Number;
    var y:Number;
}
class Bitmap extends Node {
}

I believe the equivalent could be something like this in Rust:

struct Node {
	x: f64,
	y: f64,
	subtype: NodeSubtype,
}

enum NodeSubtype {
	Bitmap(Box<BitmapNode>),
}

struct BitmapNode {
}

The subtype specific properties and methods can also be added to Node as dynamic dispatch methods. For example:

impl Node {
    pub fn src(&self) -> String {
        if let NodeSubtype::Bitmap(b) = self.subtype {
            b.src
        }
        "".to_owned()
    }
}

However, let's say Node can be a UI component. What would be the best way to allow the library user (who uses Node from another crate) to create custom Node subtypes? For example, a way to add a TextArea subtype that extends Node.

I think one way would be to add a dynamically-typed component using std::any::Any. Is there a better pattern? The downside of that is that you can't provide dynamic dispatch methods for the subtype since you can't impl Node in another crate.

The enum approach is fine if there's a closed set of node types.

However, if nodes can be arbitrary types, then you need to flip the question from "how do I insert other node types" to "what do I actually want to do with the nodes", and make a trait for that. Not dyn Any, but dyn Node.

A Box<dyn Node> could hold BitmapNode, or JustXandYNode or Some3rdPartyNode, and then put methods on the trait that cover all the things you want to do with the nodes.

You won't be able to elegantly share fields, because Rust doesn't have data inheritance, and the trait syntax that looks like interface inheritance isn't quite inheritance either.

Also instead of translating OOP design into Rust, you will probably need to rethink it and "flatten" hierarchies. Instead of BitmapNode extends Node, maybe struct Node { bitmap: Option<Bitmap> } or instead of subclasses of BitmapNode, have a trait for HasABitmap that can be attached to any node type, and have fn bitmap(&self) -> Option<&dyn HasABitmap> on the common Node trait.

1 Like

Interesting, but with a node as a collection of components, I don't think I'm able to allow the user to extend Node with custom methods... Like, this would require converting Rc<dyn Node> to something like Rc<PositionNode>, and it'd easily get verbose if I had to downcast like that everytime.

Actually, guessing more, I could provide methods specifically for manipulating the node's position. The difficulty I'll have is having to lookup for a specific component into the collection I guess?

In Java having methods that take just a Node would also require casts to access PositionNode-specific methods. However, calling PositionNode's-specific implementation/method overrides of the Node interface works in Rust just as well as in Java, without downcasts. When Node is a trait (not a struct), then &PositionNode as &dyn Node calls the appropriate implementation when you use the Node trait.

A Java Object in Rust terms is something like Arc<Mutex<dyn Object>>, so a Java-like design with arbitrary sharing, unrestricted mutation, and subclassing will absolutely involve dealing with Rc/Arcs, interior mutability, and dyn everywhere. It's something that Java's GC and VM does implicitly for you, and Rust chooses to make it very visible. Rust's Object without the wrapper types is a much more low-level construct that doesn't have a Java equivalent (at least until they add value types).

Rust traits unfortunately don't allow declaring common fields directly, so you would have to have getters and setters for the x/y fields.

2 Likes

I'd not mind the cast though! But since Node would be more ideally a collection of components, I'd also have to lookup for a specific component type. I don't have anything against a lookup too, but I'm wondering how I'd do it. Is it something like node.get::<Position>()?

This involves use of typeIds, right?

Right, I think instead of a trait for the components, I can use std::any::Any, but I never tried using a generic like that... This looks too much like ECS by the way and I think it could work pretty well.

I think I saw some project using that, but I don't know how to implement a generic method that turns a compile-time type into a typeId.

Is there a way to do this with indexing syntax (std::ops::Index) too? (No BTW...)

Here's my experiment:

struct Entity {
}

impl Entity {
    pub fn get<T: std::any::Any>(&self) {
        T::type_id();
    }
}

fn main() {
}

It doesn't work because type_id is an instance method...

The only thing I guess I can do is implement some component trait with a static id() method that returns an unique string.

That worked:

struct Entity {
}

impl Entity {
    pub fn get<T: Component>(&self) {
        T::component_type_id();
    }
}

trait Component {
    fn component_type_id() -> &'static str;
}

struct Position {
    _x: f64,
    _y: f64,
}

impl Component for Position {
    fn component_type_id() -> &'static str {
        "www.code.org/component/position"
    }
}

fn main() {
    let e = Box::new(Entity {});
    e.get::<Position>();
}

Getting the TypeId without an instance is spelled

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

But you don't use this directly when downcasting, you use downcast_ref and friends.

1 Like

By this are you saying something like the Enitity-Component part from the Entity-Component-System (ECS) pattern? I see that you wrote dyn Node, but isn't it supposed to be a struct of components rather than an abstract trait?

Assuming you meant Arc<Node> is a struct of components, would it work well if the Component trait allowed the component to access its owner Node? For example:

  • trait Component
    • fn is_subtype_component(&self) -> bool { false }
    • fn draw(&self, node: &Arc<Node>) {}
    • fn process(&self, node: &Arc<Node>) {}
    • fn ready(&self, node: &Arc<Node>) {}

Because, with Java inheritance, Bitmap being a subtype of Node means that Bitmap can access Node.

I misread it, gonna think further...

So the problem I've with the trait approach for node inheritance is that I still can't have custom methods directly in it. For example, how will the user add a new method f() to the trait? There's no way, I guess. So I still think of the Entity-Component part from the ECS pattern to do this.

In Java you can't add new methods to an interface either.

Implementors can add any new methods they like to their concrete type. You can have both impl ConcreteNode and impl AbstractNodeTrait for ConcreteNode, and the trait impl can use methods from the concrete impl.

2 Likes