[Pre-RFC] sum/union types

Summary

Introduce "sum" or "union" types. These types are defined as combination of multiple structs.

Motivation

At the moment, if there 2 types that share some properties, they have to duplicated.

struct First {
    one: String,
    two: u32,
    custom: bool,
}

struct Second {
    one: String,
    two: u32,
    foo: f64,
}

The fields one and two are duplicated between structs First and Second. This is hard to maintain and scales terribly.

One real-world example of this is typed HTML. There are over 140 HTML elements, with over 30 global attributes. Many other attributes are shared across elements. Defining such types can result in huge amount of duplicated code.

This is also a big problem in Rust UI frameworks: Ergonomic pattern for passing along on* callbacks to children · Issue #1533 · yewstack/yew · GitHub

Proposed solution

Introducing union types. The above snippet can also be written as:

struct Base {
    one: String,
    two: u32,
}

// typescript syntax
type First = Base & {
    custom: bool,
}

// could be Rust
#[extends(Base)]
struct Second {
    foo: f64,
}

// alternatively, however this requires a new keyword
struct Third extends Base {
    bar: HashMap<String, String>,
}

Going back to the HTML example from before, there could be a shared struct that multiple elements can branch off of.

Prior art

nit, but I'd suggest finding a different name for this. "Sum type" already has a programming meaning https://en.wikipedia.org/wiki/Sum_type: Rust enums are sum types. And of course Rust has unions, so these shouldn't be called that either.

7 Likes

What's the goal here? Is this only meant to reduce boilerplate when defining structs, or is this inheritance with subclassing?

If there's subclassing and it supports &mut Base, then it will have the "object slicing" problem that C++ has.

This also needs consideration of private fields and invariants. It's easy if it required all fields to be public, but it would severely limit usefulness of the feature. For example, I don't think an actual HTML DOM could be well implemented if it could only have public fields, and nothing private, and no methods to manipulate the private fields.

There has been an alternative approach proposed:

12 Likes

I would say it's both. Ideally, we would need to be able to define the extended struct and be able to mutate it. I'm not familiar with C++ so I don't see how object slicing is a problem. If you provide any resources that explain it, that would be great.

To give a concrete example from Yew, if we are creating wrapper components, we need to define every single attribute in the properties struct for the component. Ref: Function Components | Yew and Properties | Yew.
Think of a <button> element and <MyButton> component. The component is wrapping the element with addition functionality/styles/etc. For MyButton to be able replicate functionality, it must define all the properties can be passed. This gets really cumbersome. It can be solved if <button> defined what it can take and <MyButton> extended from it. If we use proc-macros as they as today, we end up with hundreds of structs with hundreds of fields and that has a huge hit on the compile times.

One other solution that comes to mind is a (compiler built-in) attribute macro that just extends the struct with the new fields. This has the benefit of reducing code duplication but the downside of not allowing proc-macros to be able to know what the final will look like, so crates like typed-builder wouldn't work.

I hope that makes it clear what the goal is.

I was looking for similiar functionality. But I want to delegate from a trait, not from a struct. That is, something like:

trait C {
    fn common(&self) -> Arc<Common>;

    delegate * to self.common();
}

Note that this paragraph gives you a solution in today's Rust: let all elements have a common_attributes: CommonAttributes field :slightly_smiling_face:

When I read your post, I was hoping to see a discussion of why this isn't enough functionality?

1 Like

Have a look at the way Servo handles inheritance. It may provide some inspiration

You can store an instance of the base struct inside the child structs, and implement the Deref trait to return that. That way it will automatically upcast to its parent struct whenever necessary.

Doing this is a common practice for newtypes, but can be used to extend the struct with additional fields as well.

You can also have many structs, all Derefing to the same base struct.

Here's an example:

Now the boilerplate becomes implementing Deref, but that can be eliminated by creating a macro for that.

I think this can do most of the things that you could do with subclassing.

See also:

1 Like

You cannot have a typed-builder pattern with that approach. Here's a demonstration of the problem:

struct Common {
    one: String
}

impl Common {
    fn set_one(self, new: String) -> Self { self.one = new; self }
}

struct Child {
    common: Common,
    two: String,
}

impl Deref for Child { get self.common }
impl DerefMut for Child { get mut self.common }

fn main() {
    let child = Child {
        common: Common { one: String::new() },
        two: String::new(),
    };
    let child = child.one("new string".to_string()); // fails here
}

This pattern is used to enforce type-safety at compile time when using builder pattern. I'm not aware of a better solution for this.


Yew also uses a pattern not dissimilar to that. You can find that out at Redo properties, take 2 by WorldSEnder · Pull Request #2729 · yewstack/yew · GitHub

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.