[Pre-RFC]: Unnamed struct types

###Summary Rust already has these kinds of structs:

struct Rgb (i8, i8, i8)

the unnamed analog is

(25i8, 25i8, 80i8)

I am proposing that this is extended to make this

struct Rgb { red: i8, green: i8, blue: i8 }

have this unnamed analog

{ red: 25i8, green: 25i8, blue: 80i8 }

in other words, tuples : tuple structs :: unnamed structs : structs

###Motivation Since tuples exist, they give a way to have duck-typed unnamed types. Tuples are defined by argument order. Structs are not. This is perfectly valid:

let color = Rgb { green: 25i8, red: 25i8, blue: 80i8 };

By using structs, you prevent errors from wrong ordering of arguments. You can get more lightweight syntax by using tuple structs in cases where your types are different since the wrong ordering is detected by the compiler. Even more lightweight are duck-typed tuples (unnamed) because they don’t need to be declared. However, Rust lacks an anonymous struct type that can be used in a duck-typed manner.

This encourages API writers to either use tuples/arrays despite the ordering hazard or to just have two arguments that are conceptually part of the same anonymous struct to be passed as two separate values.

Here’s a line from http://www.piston.rs/

        ...
        rectangle([1.0, 0.0, 0.0, 1.0], // red
                  [0.0, 0.0, 100.0, 100.0],
                  c.transform, g);

This is the kind of API design that happens when you can’t express something like

rectangle({red: 1.0, blue: 0.0, green: 1.0, alpha: 1.0},
     {position: {x: 0.0, y: 0.0}, size: {width: 100.0, height: 100.0}},
     c.transform, g);

in a very simple way without having to declare multiple types. In JavaScript, these kinds of structured data patterns are very common because of how easy they are to write. In fact, JSON is a very popular data exchange format, and the proposed Rust syntax mimics it closely.

The function would be declared similarly as:

fn rectangle(color: {red: f32, blue: f32, green: f32, alpha: f32}, 
     pose: {position: {x: f32, y: f32}, size: {width: f32, height: f32}}, ... )

Inside the function you can refer to these as color.red and pose.position.x as you’d expect.

This also attacks the issue of named arguments in an orthogonal way:

window.addNewControl("Title", 20, 50, 100, 50, true);

the API author now has the choice to write this call as

window.addNewControl({title: "Title",
    x: 20,
    y: 50,
    width: 100,
    height: 50,
    drawingNow: true});

Again, for an example of this refer to the JavaScript community. Most libraries have agreed to write their APIs this way.

Drawbacks

This has the same drawbacks in terms of traits as tuples do. That means since this is allowed:

impl Trait for (i8,) then you’d have to allow this:

impl Trait for {foo: i8,}

There’s also a syntactic ambiguity:

let a = {b: c}; //what does this mean?

If type ascription is added, it could mean a block with the value b and of type c or it could mean an unnamed struct. So possibly one value unnamed structs must be used as following:

let a = {b: c,}; //unnamed struct
let a = {b: c: D,}; //unnamed struct with type ascription

###Alternatives

Add full keyword arguments, but not unnamed structs. This way the feature doesn’t leak into block syntax/type ascription, only affecting API design. This feels more “special cased” and goes against solving things in an orthogonal way. However, this kind of a design might solve optional and default parameters more easily without having to rely on some kind of strange overloading since I have no design for optional/default parameters (how do you deal with a struct that doesn’t have the fields you want?)

18 Likes

I heavily oppose this new syntactical feature. One of the most important features of rustc is that it catches type errors at compile-time. Duck typing would be an anti-thesis to this feature. Using your example:

let color = (25i8, 25i8, 80i8);

This already compiles in stable rust into a 3-tuple. I do not expect the compiler to suddenly promote this type into a tuple struct as it sees fit without my knowledge of what’s happening behind the scenes.

Also, this new syntax creates ambiguity between structs that have the exact same fields and field types but have different semantic meaning. Consider struct GeneratedId(u32) and struct Position(u32). If we have a function fn foo(arg: GeneratedId), I want rustc to sanity-check my input to be a real GeneratedId, and not a 1-tuple binding that I defined earlier for the purpose of treating it as a Position but accidentally passed it in as an argument for foo.

3 Likes

Let me say that this is a motivated and well-written proposal, though I’m not sure yet whether I approve. I noticed one glaring omission.

In this example:

rectangle({red: 1.0, blue: 0.0, green: 1.0, alpha: 1.0},
     {position: {x: 0.0, y: 0.0}, size: {width: 100.0, height: 100.0}},
     c.transform, g);

How is the rectangle function itself defined?

fn rectangle(color: ???, pose: ???, xf: Transform, g: f32)

It seems you’d need to name the structs anyhow.

Or would you want to do this:

fn rectangle(color: { red: i8, blue: i8, green: i8 },
             pose: { position: { x: f32, y: f32 },
                     size: { width: f32, height: f32 } },
             xf: Transform, g: f32)

…which I would say is getting a bit too dense!

I’m not sure “anonymous” is the right adjective. We call closure types anonymous because you can’t write the type at all. These structs and the existing tuples are both just “unnamed” or “unlabeled”, but you should still be able to write their type.

1 Like

I am not talking about promoting anything into anything. I am talking about adding another type to the type system.

4 Likes

Alternative:

Allow elision of struct names when calling functions.

Given the below, which I believe is possible in current Rust:

struct Args {
    width: u32,
    height: u32,
}

fn area(Args { width, height }) -> u32 {
    width * height
}

area(Args { width: 100, height: 100 });

allow calling area with:

area({ width: 100, height: 100});

This does not allow elision of the type name anywhere else.

In order to elide the type, it must be imported/defined in the current module; similar to how a trait would need to be.

7 Likes

My above comment might be summarized as “how do you write the type?”

1 Like

What bothers me about this is you can’t see the type of the expression {width: 100, height: 100} without looking into another crate. I didn’t propose this because I was sure that some people would object the same way. In my proposal {width: 100, height: 100} is a concrete unnamed struct type.

In other words, you’re introducing syntactical ambiguity. What is the difference between a 3-tuple and your proposed anonymous tuple struct with 3 fields?

If we’re on the topic of types, then I’ve got even more resistance against it. If we have a let-binding to an anonymous struct, then this binding can be used as a substitute for any struct that happens to have the same fields and field types as this anonymous struct, i.e. this anonymous struct has to be of type any, which means all type checking guarantees in rustc are out of the window at the moment you use an anonymous struct.

In other words, you're introducing syntactical ambiguity. What is the difference between a 3-tuple and your proposed anonymous tuple struct with 3 fields?

The difference is it's a different type and can't be used interchangeably with tuples. It has a different syntax and the bindings are used differently.

If we have a let-binding to an anonymous struct, then this binding can be used as a substitute for any struct that happens to have the same fields and field types as this anonymous struct

This argument also applies to tuples, if you have a let binding to a tuple, then this binding can be used as a substitute for any tuple that happens to have the same types as this tuple.

I don't think you're understanding my criticism. How can you ensure that (25i8, 25i8, 80i8) is a 3-tuple, and is totally different than (25i8, 25i8, 80i8), which is an anonymous tuple struct? Your pre-RFC as it stands doesn't address the syntactical difference.

Yes, and what you're proposing is worsening the problem because suddenly all tuples are now valid to be parsed as any tuple struct. Which brings up yet another problem - should this (1+2) be evaluated as an expression? Or an anonymous 1-tuple struct with the expression 1+2?

I also don't think API authors would use plain tuples as parameters/arguments. Doing so would be bad API design IMHO.

and is totally different than (25i8, 25i8, 80i8), which is an anonymous tuple struct?

the proposed syntax is {red: 25i8, blue: 25i8, green: 80i8}

I also don't think API authors would use plain tuples as parameters/arguments. Doing so would be bad API design IMHO.

I take an array of tuples in my function:

pub fn cowbuzz<'a>(tuples: &[(&'a str, &Fn(i32) -> bool)], i: i32) -> Cow<'a, str>

the point here is that since &str and &Fn(i32 -> bool) are different types and are pretty specific, there is no way to mess this up

My impression when I first started reading this pre-RFC is that you intend to make anonymous structs out of all struct types. But what you just told me is that you want to only touch structs with named fields. Can you please clarify what kind of struct types that you’re intending to make anonymous?

I am proposing a completely new syntax.

{red: 25i8, green: 25i8, blue: 80i8}

this doesn’t currently compile as an expression. You can’t write:

let color = {red: 25i8, green: 25i8, blue: 80i8};

I am proposing this to be the new unnamed struct type. This is the only way to write it.

Declarations would similarly be {red: i8, green: i8, blue: i8}

6 Likes

The same way you write the type of a tuple: (f32, Vec<u8>) and now {a:f32, b:Vec<u8>}. Then I suppose for considering type equality the field names also have to match.

2 Likes

My argument for polymorphic typed structs still stands. Your syntax provides no way of distinguishing between struct Foo { red: i8 } and struct Bar { red: i8 }. During compilation, the anonymous let x = { red: i8 }; would have a type of any. I’m not convinced that a simplification of unnaming the struct is worth all the other problems and issues that comes with it.

During compilation, the anonymous let x = { red: i8 }; would have a type of any

That's not true, the same as as let x = (0i8,); does not have a type of Any, it has a type of (i8,)

Your syntax provides no way of distinguishing between struct Foo { red: i8 } and struct Bar { red: i8 }.

This is by design.

5 Likes

Let's focus on named struct examples, because that's what you want to change. { red: i8 } has a field named red which is of type i8. What is the type of the ensemble { red: i8 }? Do we represent it like a tuple, but with field names instead? If so, this pre-RFC already goes way out of scope of simply unnaming a struct. You would then need to treat this new construct as a subtype of both Foo and Bar. How is this not an any type?

the type is {red: i8,}

So let’s say you do:

let x = {red: 0i8,};
x + 5;

the compiler will emit an error message like so:

error: binary operation `+` cannot be applied to type `{red: i8,}` [--explain E0369]
 --> <anon>:7:5
7 |>     x + 5;
  |>     ^
note: an implementation of `std::ops::Add` might be missing for `{red: i8,}`
 --> <anon>:7:5
7 |>     x + 5;
  |>     ^
4 Likes

This raises an interesting question of whether you can impl Add for { red: i8 } (I assume no).