Efficient code reuse via "type composition"?


#1

What-up. Late to the party :stuck_out_tongue:

I’ve been slowly (almost against my will) hatching an idea for the efficient code reuse challenge. I’ve tried to contain the urge to share it, but it’s an itch I have to scratch! It works in my brain, but my brain might be broken, so I’m coming here for feedback. If you guys sort of like what I have so far, I’d be more than happy to write a detailed RFC over the next few days.

This is a “brief” description of what I have in mind. I won’t talk about how this interacts with non-struct types (such as primitives and closures), how this deals with pub and non-pub fields, the performance implications, or even how casting works, but I would in an RFC (and I will through discussion).

The basic idea is to rip off multiple inheritance, add more customization, then relabel it as “type composition” (or whatever name you guys like). Here’s the rundown:

  • Tuple structs are the mounting point for the new functionality.
  • Optionally “compose” the tuple struct to inherit all, or hand chosen trait implementations, methods, and fields.
  • Optionally map fields to component types’ fields.
  • Allow structs in type bounds for generics. This would require that the type have all of the fields from the struct, and the fields must have the same name and type.

Here’s a little taste of the DOM example:

trait IElement {
    fn before_set_attr(&self, key: &String, value: &String);
    fn after_set_attr(&self, key: &String, value: &String);
}

struct ElementData {
    attrs: Vec<String, String>,
};

impl INode for ElementData {
    fn as_element<'a>(&'a self) -> Option<&'a Element> {
        Some(self)
    }
}

#[compose_traits]
#[compose_impls]
#[compose_fields]
struct Element(ElementData, Node)

impl<T> T
    where T: IElement,
          T: Element,
{
    fn set_attribute(&self, key: &String, value: &String) {
        self.before_set_attr(key, value);
        //...update attrs...
        self.after_set_attr(key, value);
    }
}

Note that in the example, Node is also a compound type (albeit a very simple one). You can compose compound types together to create a hierarchy a la traditional inheritance.

When you look at traditional inheritance, each class in the hierarchy has its virtual methods, its methods, and its member fields. In Rust, this is all nicely separated for us. Traits provide a means for virtual methods, impl blocks provide a means for methods, and structs include their fields.

Looking at the DOM example, there are 3 compiler attributes above the compound Element, corresponding to the virtual methods, methods, and fields. By default, the compose attributes will inherit everything. You can also explicitly choose what to inherit:

#[compose_traits(INode)]
#[compose_impls(
    some_method1: ElementData::some_method,
    some_method2: Node::some_method,
)]
#[compose_fields(NodeData{parent: some_field_name})]

Note: Regarding compose_fields, all data is always part of the new compound. Inheriting a field means that a field is created on the compound that maps to a field in a component struct. Inheriting a field simply provides ergonomic access.

The compose_traits attribute takes traits that you want to inherit. By default, when a trait method is called on a compound, the respective implementation for each component is called. For methods that return a value, the value of the “child-most” component is used. The “child-most” component is the earliest component in the tuple struct parameters to implement the trait. You can also explicitly implement a trait for a compound type:

impl INode for Element {
    fn as_element<'a>(&'a self) -> &'a Element {
        Node::as_element(self); // Useless hurray
        ElementData::as_element(self)
    }
}

compose_impls takes specific methods, and allows you to rename them in the compound type. You can also list a type if you want all of its methods.

compose_fields uses the destructuring syntax to map component fields to their new names in compound type.

Finally, we’ll talk about the portion which I’m most unsure about: implementing abstract classes. Technically, in traditional OOP, Element from the DOM example is an abstract class; it can’t be allocated. It has a method, set_attribute, that makes use of virtual methods not implemented yet (before_set_attr and after_set_attr). To handle abstract classes, my current idea is to allow type bounds to allow requiring a component. Essentially, a component is just a tuple field. This has some weird implications, and I hope there’s a better way.

impl<T> T
    where T: IElement,
          T: Element,
{
    fn set_attribute(&self, key: &String, value: &String) {
        self.before_set_attr(key, value);
        //...update attrs...
        self.after_set_attr(key, value);
    }
}

The impl header looks awfully weird for something that’s simply trying to say “implement for types that have Element as a component and implement IElement”. I need to think about how this will work more.

What I like

  • This keeps everything separate. Traits don’t get to have fields like they do in another RFC.
  • This makes the “newtype pattern” more ergonomic. For example, you can inherit the Add trait for Centimeters.
  • It’s easy to grok (I think). Just smashing types together!

What I don’t like

  • I’m not satisfied with the compose_impls syntax.
  • As I said, implementations for traditional “abstract classes” don’t feel quite right. You could write a IElement implementation for a tuple type that has a Element field, then call set_attribute on it. Funky business.
  • I’m not satisfied with the way trait methods that return a value is handled. Using the “child-most” component’s return type just feels hacky.
  • In theory, you could compose a type with multiple components of the same type. This doesn’t really make sense, and is hard to handle in the abstract class impl case. We could forbid multiple components of the same type, and use a new keyword like compose for type composition so that you can’t compose tuple structs. I don’t like this very much either.
  • The struct as generic type bound concept in general is not well thought out on my end at all. Need to think on it more.

What do you guys think?

It definitely needs a lot of refining, but what do you guys think about the overall concept? Should I write an RFC, or is the idea unworkable?

If you think it is workable, and have suggestions for any of the issues I mentioned, please share!

Hope I’m not overstepping… “Who’s this guy? Hasn’t made a single commit to Rust? Proposes weird new features? Hah!”