[Pre-RFC]: Unnamed struct types

The {...,} syntax is rather strange as well.

It's to avoid syntactic ambiguity in the case of a single member struct. The same syntax is used for tuples like (i8,)

One of them is encouraging long, untyped argument lists, which is generally considered an anti-pattern.

Currently those things are implemented by tuples (f32, f32, f32, f32) or arrays [f32, 4] which is even less clear. What does that argument even mean? If I give you an API that has an argument of type {red: f32, green: f32, blue: f32, alpha: f32} I would consider this to be very clear intent. I don't consider Rgba {red: f32, green: f32, blue: f32, alpha: f32} in any way more clear.

Furthermore, I think it's much easier to pass around {red: f32, green: f32, blue: f32, alpha: f32} - different crates can use this exact format by agreement. If you have Rgba {red: f32, green: f32, blue: f32, alpha: f32} for this purpose - then where is it declared? Which crate does everyone else import this type from?

1 Like

You can use irrefutable pattern matches in fn signatures, including on named structs in rust today. Rust Playground I don't see why their anonymity would make a difference.

2 Likes

I don't think this is what this pre-RFC is suggesting. On the motivation section, there's this example:

It is not suggesting that {foo: u8,} and struct Foo { foo: u8 } are different and non-interchangeable types. It is trying to instead meld the former into the latter type.

1 Like

Whether @iopq intended it or not, I think the clear consensus here is that they should be distinct, just as tuples and tuple-structs are distinct.

4 Likes

My sentiments are shared with @withoutboats, if simply a tuple with named fields are being introduced, then I'm fine with it. What I am strongly against is treating this new type as a substitute for structs in parameters (which I think is the true purpose of this pre-RFC).

I'm pretty sure that is not the purpose of this pre-RFC.

Anyway, I have become aware of a realization that would prevent me from using this in winapi. Currently you cannot do #[repr(C)] on anonymous tuples, so similarly you wouldn't be able to #[repr(C)] on anonymous records either, which means I can't actually use them in winapi. Looks like I'll be stuck with naming all my types :frowning2:

I still like this pre-RFC though.

2 Likes

Iā€™m in favour of this feature and think it is a step in the right direction towards a named field solution. I do think there are some nitty details to address (canonical ordering, repr attributes, parsing ambiguities, the .. syntax, etc.), mostly suggested in this thread.

I wouldnā€™t worry too much about defaults for now, I would like to see (separately) an RFC for default struct fields (I donā€™t think Default is a complete solution even for named structs).

6 Likes

@nrc I'd go for the simplest solutions to your nits:

canonical ordering

Alphabetical. (I originally went with arbitrary, but on further thought it's not like it poses a back-compat issue.)

repr attributes

Not initially supported.

parsing ambiguities

Use a comma, exactly like with tuples. (x) and {x} are already basically synonymous, so it makes sense to "escape" them the same way as (x,) and {x: y,}.

the .. syntax

Seems like that's best left to a different RFC.

4 Likes

I donā€™t think any of those are as simple as that. There are a bunch of trade-offs for all of these things. At the least, some investigation needs doing.

I meant by the API author, not by the compiler. Iā€™ve updated the post.

What difficulties are you envisioning?

When it says "this might be changed to", it doesn't mean that there will be a magical coercion; it means that the API author will change the API to use anonymous types.

1 Like

Briefly:

canonical ordering

We almost certainly want to allow the compiler to choose optimal orderings, but at the same time they must be consistent, and for C interop we will want to allow the ordering to be specified.

repr attributes

Initially supported or not, we need to ensure that the design can accommodate them somehow and that they can be added backwards compatibly (which is a big ask since if they affect the ordering this must be an implicit part of the type).

parsing ambiguities

The parser has all kinds of special rules around structs, blocks, and braces. Treatment of : is not exactly straightforward either. I'd want to be sure that we can in fact add anon structs without breaking these subtle special cases. Saying "just use a comma" is insufficient.

the .. syntax

Again, even if it is not in a first implementation, we need to think about how it will be incorporated in the future and that there is room in the design for it, plus backwards compatibility.

Here and in the case of repr attributes, we would want to know now whether we could support these aspects in order to assess the desirability of the feature as a whole.

By "canonical ordering" I assumed you were talking about things like Debug and printing types.

The internal ordering is of course unspecified, just like every other non-repr struct in Rust. It doesn't make sense to let people specify the order because that creates a distinction between {x: i32, y: i32} and {y: i32, x: i32}, but those types are identical. Any way of enforcing order is better done through writing a nominal type.

we need to ensure that the design can accommodate them somehow

Why? Nobody has needed them so far. I believe tuples aren't FFI safe either - at least the Rustonomicon says

DSTs, tuples, and tagged unions are not a concept in C and as such are never FFI safe.

Anonymous structs as proposed are also "not a concept in C", so they would equally not be FFI-safe.

Saying "just use a comma" is insufficient.

Fair enough on this one - the grammar needs to be more than just unambiguous.

we need to think about how it will be incorporated in the future

I don't see how .. can cause a back-compat issue.

A suggestion which sidesteps most of @nrcā€™s concerns (except parsing) would be to treat {a: i8, b: u32} as a ā€˜struct literalā€™ similar to ā€˜integer literalsā€™ whose specific type gets resolved by inference. So youā€™d have

struct Options {
   foo: String,
   bar: f32 = 2.5,   // default args
   baz: u32 = 10,
}

fn do_something(options: Options) { /* ... */ }

do_something({foo: "oh no!".to_owned()});

let options = {foo: "hello".to_owned(), baz: 24};
/*...*/
do_something(options);  // type of `options` gets resolved to `Options` and
                        // missing arguments filled in.

Layout, repr etc still get decided on the Options struct, without any additional syntax, but you still gain the ergonomics (in particular around not having to import additional types). @iopq suggested types can be added backwards compatibly (with unspecified layout/reprā€”similar to tuples) and can serve as the i32 fallback equivalent to a struct literal.

8 Likes

This feels very much like std::initializer_list (which isnā€™t a bad thing).

3 Likes

I missed that connection, but itā€™s a good point, especially in the fallback to unnamed struct type case, since initializer_list is nameable after all.

It also feels a lot like initializer_list if used as a return value:

struct Foo {
    x: Vec<u32>,
    y: u32,
}

impl Foo {
    fn new() -> Self {
        { x: Vec::with_capacity(16), y: 20 }
    }
}  

I think thatā€™s pretty rad.

1 Like

One place Iā€™d like to note that this would be quite useful is in associated types, and another is in defining purely internal field types with less boilerplate. To abuse the classic example of shapes:

struct RectangleClassic {
    width: u64,
    height u64,
    red: u8,
    green: u8,
    blue: u8,
}

struct RectangleTidy {
    dimensions: {
        width: u64,
        height: u64,
    },
    color: {
        red: u8,
        green: u8,
        blue: u8,
    },
}

This occupies a nice point in between writing things out completely flat (RectangleClassic), using classic tuples, and factoring things out into entire types of their own. Unlike RectangleClassic, members with correlated behaviors are kept together. Unlike tuples (but like RectangleClassic), each field has a meaningful name. Unlike creating new nominal types, boilerplate is still kept to a minimum.

in addition, I feel these would provide a valuable midpoint along any eventual refactor from RectangleClassic to distinct types - while tuples would force a garden-path divergence where the developer removes names and then re-adds them, and custom types require #[derive] boilerplate for Copy, Clone, Debug, etc, these types (presuming that, like tuples, those traits are sensibly defaulted) have none of those drawbacks.

9 Likes

So IIRC C anonymous structs in anonymous unions (as opposed to other anonymous thing in non-standard C), would be served by this. So a #[repr(..)] is something weā€™d want. I do like the literal idea, in that the repr should only be on a type so one would do ({ foo: x, bar: y }: #[repr(C)] { y: /* comes first*/ int32, x: ) or something to disambiguate.

I quite like @cristicbzā€™s ā€œstruct literalsā€ idea here.

Thereā€™s a nice symmetry here:

let i = 0i32;
let p = Point { x: 0, y: 0 };
// or
let i : i32 = 0;
let p : Point = { x: 0, y: 0 };

Inference would still need to unambiguously find the type, all the #[derive] stuff goes in the usual place, but where something else made it clear, you donā€™t have to say what it is explicitly.

That gets the usage of the API to be exactly the same as proposed in the OPā€™s piston example. And I donā€™t think that itā€™s unreasonable for the API designer to make named structs for such things. Certainly I could imagine things like Position::Origin to pass instead of { x: 0.0, y: 0.0 }.

Default arguments would end up looking like Foo({ a: 32, .. FooArgs::default() }), I guess? Thatā€™s not all that bad, actually, for a purely library solutionā€¦

(Hmm, I wonder whether .. syntax would be sufficient to infer a type for a ā€˜struct literalā€™?)

1 Like