[Pre-RFC]: Unnamed struct types

I don’t see why not, you can impl Add for (i8,)

2 Likes

@KiChjang I think you have misunderstood this proposal. The comments about structs / tuple structs were an analogy, there’s not a proposal to add any sort of coercion or subtyping at all.

This proposal is just to allow you to create a type like { x: u32, y: u32 }, which doesn’t have a name. This type would be distinct from all other types - that is even if you had a struct Point { x: u32, y: u32 }, you could not coerce a { x: u32, y: u32 } into a Point. This is just a new sort of unnamed type, similar to tuples, except that instead of having positional members, the members have names.

15 Likes

To me this seems consistent and fine - the kind of feature a user could infer to exist from knowing the other features of Rust - but I’m not thrilled by the idea of trying to use this to implement named arguments.

7 Likes

Well kinda, you can impl MyTrait.

As I understand it, this RFC is to structs with named fields as tuples are to tuple structs. I like this RFC, I like it very much indeed. I’m pretty sure there’s a bunch of places in winapi where I could totally take advantage of this to closely match what the headers are doing. If this goes through, then all I’ll need is a similar proposal for anonymous unions and oh man will it be amazing.

+1

13 Likes

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.

12 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.