[Pre-RFC]: Unnamed struct types

My main concern here is with the layout of the records. Intuitively,

{ a: 1.0, b: 0, c: "foo" }

and

{ b: 1, c: "bar", a: 0.0 }

should be same type. However, the layout of the data in the type is important. If we just use the ordering in the source, then the first would be (f32,i32,&str) while the second would be (i32,&str,f32). Javascript objects are just dictionaries, where access is effectively a lookup in a hashmap. Field access in Rust is a pointer offset, so determining how to lay the data out is important.

7 Likes

@Aatch I’d be in favor of a solution where an anonymous record is “canonicalized”, aka the fields are sorted in canonical ordering so that your two examples would have an equivalent type.

11 Likes

Those two should definitely be the same type, with the same layout, though there should be no guarantee about what specifically that layout is.

2 Likes

There’s no coercion going on, only new types. Anonymous structs are not coerced to tuples.

Just how (u8, ) and struct Foo(u8) and struct Foo{foo: u8} are different types, {foo: u8,} is also a different type and not interchangeable with {bar: u8,}, or any of the other types I just named.


A slight issue with the proposal is that you can’t implement Default everywhere to get optional kwargs. {foo: u8, bar:bool} is not a local type so you cannot impl Default on it. This means that using the splat syntax to initialize just one parameter ({foo: 1, ..Default::default()}) won’t work. You can do this with a named struct because it is a local type. But as a named struct, callers have to import and name it, which is noisy. We could add autocoercion from anonymous structs to named structs with the same fields (public) but then you have new classes of type errors about ambiguousness and all of KiChjang’s concerns apply.

Since default kwargs is a huge plus point of kwargs in other languages, I personally prefer if any proposal for kwargs had the ability to default them.

3 Likes

Everything old is new again. :slight_smile: Rust used to have this feature 5 years ago. In fact, it was the only way to make what are now called structs at the time–nominal structures wouldn’t exist for several years.

11 Likes

Here's the issue where they removed in the first place.

And some more recent discussion on the topic

6 Likes

I do see that issue with using unnamed structural types for optional args. That said, if keyword arguments end up being awkward with unnamed structs, proper optional keyword arguments can be added later. There is a benefit to just adding unnamed structs now since it mirrors tuples directly anyway.

I’d be interested in a way to #[derive()] some traits for those structs: Debug, Clone and serde’s Serialize come to mind. Maybe annotate the variable construction this way?

fn foo() {
    #[derive(Clone, Debug)]
    let bar = {x: 1,};
    let baz = bar.clone();
    println!("{:?}", baz);
}

Or separately, with the type “definition”?

#[derive(Clone, Debug)]
struct {x: i32,}

Or without the struct, as written in the original post?

#[derive(Clone, Debug)]
{x: i32,}

An advantage of these structs it that it allows to build a struct with unnamed/unnamable members (if those come from impl Trait-returning functions - iterators I’m looking at you).

To come back to Serialize, I wonder how feasible something like this would be:

fn bar() -> impl Serialize {
    #[derive(Serialize)]
    {
        y: 1,
    }
}

fn foo() -> impl Serialize {
    #[derive(Serialize)]
    {
        x: bar(),
    }
}

fn main() {
    serde_json::to_writer(&mut ::std::io::stdout(), &foo());
}

I’m personally not a fan of using this to solve the issue of named arguments; this means that named arguments are opt-in when you decide to use one of these unnamed structs as arguments in your functions, and it also makes the code more verbose when using these arguments (args.my_named_arg).

As for optional/defaults, some (procedural) macro could, given a default value for each variable, generate Into<complete unnamed struct> for every partial unnamed struct (we could restrict the variables being optional). Finally, just have the function take an Into<complete unnamed struct>. Though I’m not sure it can be done in an elegant syntax :S

I don’t see how you can impl traits that you don’t define yourself for these types. Anonymous structs would have no canonical definition place, so the only place allowed for trait impls would be where the trait is defined.

Otherwise, what stops two places in the code from defining conflicting impls of Add for {red: f32, green: f32, blue: f32}? You can’t even say that two otherwise identical anonymous structs are different when defined in different modules, since that would make them unusable as function arguments - a function taking crate1::module1::{foo:i32} cannot be called from crate2::module2, since the literal {foo:10} written there would be a crate2::module2::{foo:i32}.

I don’t see how you can impl traits that you don’t define yourself for these types.

You can’t, actually. You can only define it where the trait is defined.

Omitting the type is one thing (although I do oppose that as well), but literally allowing for closure-like struct types is a quite big change with little benefit.

Despite, there is a parsing ambiguity, there is a parsing rule, '{' EXPR '}', which this clearly breaks given that EXPR := EXPR ':' TY | ....

Secondly, there are a lot of other problems here. One of them is encouraging long, untyped argument lists, which is generally considered an anti-pattern.

Now, introducing such an feature would mean splitting the whole ecosystem into two, incompatible forms of passing arguments.

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

I could go on, but my general opinion is that this is a very bad idea with little benefit.

1 Like

Also, tuples aren’t duck types. Semantically, they are equivalent to structs.

[quote=“ticki, post:38, topic:3872, full:true”]Despite, there is a parsing ambiguity, there is a parsing rule, ‘{’ EXPR ‘}’, which this clearly breaks given that EXPR := EXPR ‘:’ TY | …[/QUOTE]

Anonymous structs with one field certainly won’t be used that much, but you can still require them to be written with a trailing comma { x: i32, }, which should solve the parsing problem.

[quote=“ticki, post:38, topic:3872, full:true”]Secondly, there are a lot of other problems here. One of them is encouraging long, untyped argument lists, which is generally considered an anti-pattern.[/QUOTE]

You can say this for pretty much every feature, that there are good and bad uses of and I can see how anonymous structs can help solving the named/default parameter case.

I wouldn’t consider it to be the nicest syntactic way for named/default parameters, but all the other ways have their own issues, which might even have a bigger impact on the whole language, and any solution for named/default parameters will most likely be opt-in, so there will be a difference between functions/methods with and without named/default parameters.

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. https://is.gd/TeJQGx I don’t see why their anonymity would make a difference.

1 Like

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.

3 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).