Nested struct declaration

I was wondering if having nested struct declarations could be a nice feature for Rust. In Rust and most of the languages we declare structs this way:

pub struct Parent {
   pub child: Child;
}
pub struct Child {
  ... // Some props
}

when sometimes the Child would not exist without the parent. I think something like this could be nice:

pub struct Parent {
  pub child: pub struct Child {
    ... // Some props
  }
}

We can discuss the details but will be nice to hear your opinions

1 Like

I guess my question is: what problem would it solve, exactly?

Please keep in mind that privacy issues (e.g. Which types are publicly exported and which are not) are currently elegantly solved by the use of modules.

1 Like

Surely a user wouldn't construct Child if there's literally no way to use it other than with Parent? Even if they did, what's the harm?

Structural records would give you the nesting (without the privacy controls or naming).

// From the RFC
struct RectangleTidy {
    dimensions: {
        width: u64,
        height: u64,
    },
    color: {
        red: u8,
        green: u8,
        blue: u8,
    },
}
7 Likes

First of all, thank you for your desire to improve Rust. I would like to hear more about what the expected benefit of this would be.

From a rustc implementation perspective, this would be possible to do in a relatively straightforward manner. But there are some drawbacks that need to be considered.

rustc is not the only tool that needs to understand the Rust grammar, whenever we change the language, either in its syntax or its semantics, we need to be mindful of how these changes will affect other implementors, like rust-analyzer or mrustc.

Even if the parser is able to understand the new syntax, it doesn't mean that a human can. In this context it does seem easy to follow, but the more complex the container-containee relationship becomes, the harder it will be to read. John Carmack once said "if something can syntactically be entered incorrectly, it eventually will be". In order for rustc's fame as "friendly" to hold, we need to anticipate how invalid syntactical constructs would be handled. For code like struct Parent { child: Child {}}, struct Parent { child: struct {}} or struct Parent { child: pub } we need to have enough context in order to help 1) the parser recover and continue and 2) the user by recognizing what they were trying to do and explaining to them why it isn't allowed. Complicating what can go in certain places makes it harder for to us differentiate from likely intended possibilities.

Having more places to define ADTs makes it harder for people to skim an unfamiliar codebase. You could argue that we've already gone down this route by allowing impl blocks in expressions, which have non-local effects, but I would qualify that as a "grandfathered-in mistake" that we should avoid to repeat.

We need to consider how this would interact with proc and derive macros, particularly things like #[derive(Default)]: would it flow down? Would each field newtype need an annotation?

We would also need to consider how this would interact or preclude potential future features, like recently restarted conversation around field default values. How would this syntax compose (both when it is correct and when there are missing tokens or typos) with something like:

struct Parent {
    pub child: Child = Child::Empty,
}
enum Child {
    Empty,
    Parent(Box<Parent>),
}

Finally, you have to consider how you would refer to Child, what would it's path be? crate::Parent::Child? crate::Child? The former would be confusing because it complicates the understanding that ADTs are not modules, and even if made to work would be problematic on the face of name clashes with associated types. The later would be confusing because the type's path wouldn't match the code the user sees. How this would be taught to people is so important that it is an explicitly required section of the RFC process.

Because of all of these points, I would consider this feature to be more trouble than it is worth. That being said, please do not be discouraged by my position. There are plenty of features I have thought of that sounded great but upon closer inspection were problematic or rejected for one reason or many.

With that out of the way, I would like to hear more from you about the intention behind the feature request, as I might be missing a big benefit this feature could bring, but I think that there already existing or proposed alternatives that could work better. If the intention is to make the grammar be more expressive, I think that the raised points overpower that desire. If it is to have the common pattern of dynamic languages of having rigid map trees in a type-safe language endorsed manner, @quinedot's point of using structural records would be a better fit. If it is to make Child only constructable when used in a Parent, there are design patterns that could be used (and even uplifted into language features), like builders or making Child private and only constructable from a method on Parent. And of course, we always have macros, where we can try things out and experiment with different behaviors in the real wold before attempting to uplift them into the language.

9 Likes

Thanks, this is the feature I was looking for, I didn't find it. Sorry for open the proposal.

2 Likes