Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New struct syntax #866

Closed
wants to merge 6 commits into from
Closed

New struct syntax #866

wants to merge 6 commits into from

Conversation

phaux
Copy link

@phaux phaux commented Feb 16, 2015

Foo{x=>1, y=>2} struct syntax. Hopefully an improvement over #841. I tried to explain the implementation details more thoroughly.

Rendered

@phaux phaux mentioned this pull request Feb 16, 2015
@iopq
Copy link
Contributor

iopq commented Feb 16, 2015

As far as declaration, I agree with this post:

#841 (comment)

that's exactly how it makes sense in my head, so I would prefer my declaration syntax. This matches how we declare functions - functions have types in the signatures like foo(a: Bar) and they're called with foo(bar) not //foo(a: bar) like structs are.

I would never use the shortcut syntax because of two reasons:

  1. Most diffing tools are too stupid and do line diffs so I do everything on its own line when there's multiple items
  2. Mismatches would be more likely, especially if all the things are i32 and so forth. We use structs to becomes MORE type safe because arguments are named and passing a struct is checked at compile time. But the types of the insides of the structs are usually going to be basic types like f64 and so forth so it will be easy to put things into the wrong slots.

That said, I would prefer the "fake tuples" syntax if we were to have it over any "new" syntax because it looks more Rusty.

```

Note how part of the function argument pattern reads `x: x1: f32`.
The syntax might not be completely ambiguous, but it definately is confusing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: definitely

@CloudiDust
Copy link
Contributor

+1 to this RFC, but I'd propose that we leave the struct declaration syntax unchanged.

I am aware that you think my proposal violates the "initialization follows declaration" rule, however:

I believe all instances of "initialization follows declaration" can be reinterpreted as "declaration and initialization omit different parts of a full syntax", once type ascriptions are landed.

And the latter rule, which I think is more consistent, also avoids giving => dual purposes regarding structs, or using : or => to specify types depending on context.

@CloudiDust
Copy link
Contributor

It seems that my line notes failed to be line notes. Sorry.

@iopq, I think the "fake tuples" shortcut syntax is indeed a good variant of tuple-pattern based multiple assignment. The fact that it uses => instead of = both signifies that fact, and makes it better suited for being used in patterns.

@CloudiDust
Copy link
Contributor

A vague proof why all data structures that obey the "following" rule also obey the "omitting" rule:

Let's say a slot is a position in a "following"-rule-obeying data structure's syntax, where a type is expected in declarations, and a value expression is expected in initializations.

For any "following"-rule-obeying data structure D, the so called full syntax F is just like its declaration syntax, but with the types in every slot replaced by ascripted expressions of the form e: T.

Now, we can say, the declaration syntax of D is F with the e parts in every slot omitted.

And the initialization syntax is F with the : Tparts in every slot omitted (optionally).

(Note: my interpretation for tuple structs in the diff comments is more complex than necessary, the .0 => parts were not needed at all.)

@skade
Copy link
Contributor

skade commented Feb 16, 2015

I'm surprised by this: this is an RFC essentially saying that every syntax element should just be used for one thing, by example of ":", going as far as calling it "weird syntax" in the alternatives section. Then it moves on to suggesting "=>" and happily admits that it is already used in the match syntax - overloading that one instead.

I'm still opposed to the change completely: I would prefer to keep the old syntax and still don't see examples that would be impossible without type ascription syntax further down (even if more verbose).

It introduces a lot of noise into the ecosystem, on the final stretches to stability. There was a decision to finally release Rust - for better or worse, to start using it as a practical language, even if it has some warts. That's a wart I can certainly live with.

@CloudiDust
Copy link
Contributor

About match, I just happen to find a nasty ambiguity:

match Foo { a => b, c => d } ...

Is this matching Foo against a and c, or is it matching Foo { a => b, c => d} against something else?

@CloudiDust
Copy link
Contributor

@skade, without a syntax change, we would not be able to introduce keyword arguments later without some hacks, like the . in {. x: X, y: Y}, which I proposed before.

However, given that this change would introduce ambiguities into match expressions as I just found out, and match is a very important feature in current Rust, I'd say changing the syntax would have dubious benefits.

@skade
Copy link
Contributor

skade commented Feb 16, 2015

@CloudiDust I can also live without keyword arguments. Never put much weight on them, they tend to lead to huge parameter lists where passing a struct might just be better. And for short lists, I don't see them that important.

@CloudiDust
Copy link
Contributor

@skade, personally I think keyword arguments (and default arguments) are nice things to have (especially, over ad-hoc function overloading).

But when we try to give them a consistent syntax, we should not introduce ambiguities into match. And current features > future features if a choice must be made.

I hope something like {. foo: 1, bar: 2} and foo(1, 2, .baz: 3, .qux: 4) can be acceptable for keyword arguments if they are introduced.

@CloudiDust
Copy link
Contributor

@skade, also, it is already largely agreed that Rust's keyword and default arguments are going to be sugars of struct arguments.

Using my syntax, .baz: 3, .qux: 4 is the "expended" form of {. baz: 3, qux: 4}, which is a type-inferred struct literal used as the last argument of the function foo.

@phaux
Copy link
Author

phaux commented Feb 16, 2015

@iopq

This matches how we declare functions - functions have types in the signatures like foo(a: Bar) and they're called with foo(bar) not foo(a: bar) like structs are

// Struct declaration
struct <ident> { <ident> => <type>, <ident> => <type>, ... }

// Function declaration
fn <ident> (<pattern>, <pattern>, ...) -> <type>

// Struct initialization
<ident> { <ident> => <expr>, <ident> => <expr>, ... }

// Function initialization
???

// Struct pattern matching
<ident> { <ident> => <pattern>, <ident> => <pattern>, ... }

// Function pattern matching
???

You're comparing apples to oranges here. If we used : for declaration, we would lose the symmetry for absolutely no reason. I didn't hear any solid argument yet for why would we want to break the use-follows-declaration rule.

Remember that the declaration is meant to be "backwards" and : is now actual operator in patterns and expressions. Using : in struct declaration would be actually inconsistent, because it would then have another use over it's purpose of type ascription, which can only be valid in expressions and patterns (and struct def is just <ident> => <type>). (I know it's used for Trait bounds too, but it's a completely different context so I think that's okay)

@CloudiDust

About match, I just happen to find a nasty ambiguity

Nice find, but I think that compiler wont have any problem with it and we can tell which block is a match block and which is the struct body by looking at the word before it. If it's a path (Foo or foo::Bar), it's a struct, else it's match block.

// match expression signature
match <expr> { <pattern> => <block>, ... }
// struct literal signature
<path> { <ident> => <expr>, ... }
// struct used as expr in match
match <path> { <ident> => <expr>, ... } { <pattern> => <block>, ... }

There's no syntax ambiguity, it could be visually confusing at most.

@dpc
Copy link

dpc commented Feb 16, 2015

About the ambiguity. Can you just change => into ->?

@phaux
Copy link
Author

phaux commented Feb 16, 2015

@dpc I wanted to leave the -> operator for future use. It would be associated with functions and closures. I touched upon it briefly in the future improvements section of the RFC (I actually wanted to write a RFC about just that, but it would rely on new struct syntax anyways).

More precisely, because every function might be defined in the future as having one input value (usually a tuple, but we would make structs possible as input value to introduce named arguments) it would allow us to drop || from closure syntax (just the -> sigil would suffice).

@CloudiDust
Copy link
Contributor

@phaux, Rust's paths are expressions too, if I am not mistaken, bare variables and type names are all path expressions. I think the above snippet will always be parsed as matching Foo against {a => b, c => d} by the parser (but not by programmers)

It's like bare {x: y} with type ascriptions, the parser will parse it a certain way (as a block, not a type-inferred struct literal), but that may not be what we expect. "Visually confusing" is itself the problem.

@phaux
Copy link
Author

phaux commented Feb 16, 2015

@CloudiDust Paths of enum variants and unit-like (empty) structs are expressions, but they can't be followed by a block. Seems pretty unambiguous to me.

Edit: It turns out that having a struct literal as a match expression is not allowed anyways. It's always interpreted as the opening of the match block, even if it's struct literal block.
Playpen

set_color(Color(0: u8, 0, 0)); // initialization
let Color(red: u8, grn, blu) = get_color(); // pattern matching

struct Color<T>{r=>T, g=>T, b=>T}; // declaration
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer

struct Color<T> {
    r: T,
    g: T,
    b: T
}

as it is still type ascription.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya, I'm not sure why => is used as "has type" here. Wasn't the idea to have : mean "has type" everywhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmesmon The reasoning behind it is right there in the RFC.

TL;DR: Deciding on => or : is cosmetic, but using => is actually a little bit more consistent if you take into account the use-follows-declaration rule and the fact that type ascription is an operator that can be used in expressions and patterns only.

Seeing that PR was not outright closed, I will push a change to the declaration syntax so we can have some kind of consensus on this issue.

@CloudiDust
Copy link
Contributor

@phaux, I'd say this is expected behaviour given Rust's grammer (I should have realized this when I posted my comment above, the "always" bit. :-P)

Rust has (for the most part) an LL grammer so there should be no ambiguity as far as the parser is concerned. But programmers aren't LL parsers.

@skade
Copy link
Contributor

skade commented Feb 16, 2015

@dpc -> is used in the function definition. Same thing as with : and =>: I thought the point is to avoid overloading syntax?

@iopq
Copy link
Contributor

iopq commented Feb 16, 2015

@phaux how do you tell whether match Foo { ... } refers to a struct or an identifier?

http://is.gd/RiEOjX

you see that the error is in the second block because it's not valid block syntax
but actually it COULD be a valid record syntax if anonymous records exist

although I might be confused, but it looks like the parser might have to backtrack to handle this ambiguity?

@phaux
Copy link
Author

phaux commented Feb 16, 2015

@iopq Anything involving curly brackets is not allowed in match and this RFC doesn't change that.

@iopq
Copy link
Contributor

iopq commented Feb 17, 2015

@phaux doesn't that mean the parser has to backtrack or look ahead?

@CloudiDust
Copy link
Contributor

@iopq, the parser will always parse it as matching Foo against { ... }, whether that's what the programmer wants or not.

Your playpen code will still fail to compile even if records are added to the language, only that it will not be a parsing error then, but one with semantics. (It will be a match expression directly followed by another expression, and it is a semantic error).

So basically while what I find has visual ambiguity, it doesn't compile anyway.

Still when using the proposed => syntax in structs, there may be visual ambiguities if matches are also involved. It would be better if we have planty of time to try this on real codes and see if it actually hurts code clarity. (For testing snippets, they look fine to me, as matches and struct patterns are all clearly delimited, but that's just me and just testing snippets.) However, "planty of time" is not what we have now.

@iopq
Copy link
Contributor

iopq commented Feb 17, 2015

There's basically only ONE more arrow construct that I could think of.

~>
as in Color{ red ~> 1.0, green ~> 0.0, blue ~> 0.5 }

@CloudiDust
Copy link
Contributor

@iopq, ~> is a new operator which is not that different from ->, and I cannot think of a commonly used language that uses ~> for struct or map syntax.

@skade
Copy link
Contributor

skade commented Feb 17, 2015

I'd like to suggest rejecting this and #841. Both don't represent anything actionable and don't take the potential damage (massive churn in all existing code) into account.

Both are speculative not only on the path they could take for their syntax, but also on the syntax of future features. We've already burned through the third arrow style in this discussion alone, #841 not being much better.

@CloudiDust
Copy link
Contributor

For reference, here is a possible extension to the current syntax that can do type ascription, type-inferred struct literals/patterns and named arguments without breaking changes.

The basic idea is using . as a "mode switcher":

{x: y} is a block containing an ascribed expression, but .{x: y} is an anonymous/type-inferred struct literal.

A pattern Foo {x: y}/Foo {x: y: z} is binding the value of the field x to a local variable y (of type z), but Foo {.x: y} is binding the value of the field x to a local variable x of type y.

Examples:

let foo1 = {value: Foo}; // block
let foo2 = .{field: foo1}; // struct

// bind to other name:
let .{x: point_x, y: point_y} = get_point();

// bind to other name and ascribe:
let .{x: point_x: f32, y: point_y: f32} = get_point();

// bind to same name:
let .{x, y} = get_point();

// bind to same name and ascribe:
let .{.x: f32, .y: f32} = get_point();

// function with an anonymous struct as the last argument for supporting named/default arguments
fn draw_rect<N: Float>(.{x: N, y: N, w: N, h: N}) {}

// calling the function
draw_rect(.x: x1: f32, .y: y1, .w: x2 - x1, .h: y2 - y1);

This solution is less consistent and seems more arbitrary compared to this RFC's solution. (But the fact of it not being a breaking change is an advantage.)

@CloudiDust
Copy link
Contributor

Along the line of the previous non-breaking solution, there is another possible breaking solution that:

  1. Solve the possible ambiguity and "heavy-weighted-ness" of => introduced by this RFC's solution.
  2. Solve the "confusing pattern syntax" problem introduced by the "C99 designated initializer" (.ident=) syntax.
  3. Solve the "arbitrary switch" and other ambiguities in the above conservative solution.

The solution is: instead of using => or .ident= as the new binding syntax, we combine .ident= with the current : and use .ident:, that is:

Current: Foo {bar: 42} => Proposed: Foo {.bar: 42}.

And the declaration syntax remains unchanged.

Basically, if we see a . before an identifier and a : after it, then that is a value binding, otherwise it is specifying a type/trait bound.

Examples:

// struct declaration, not changed, as it is types that are after field names:
struct Point { x: f32, y:f32 };

let foo1 = {value: Foo}; // block
let foo2 = {.field: foo1}; // anonymous/type-inferred struct

// bind to other name:
let {.x: point_x, .y: point_y} = get_point();

// bind to other name and ascribe:
let {.x: point_x: f32, .y: point_y: f32} = get_point();

// bind to same name:
let {.x, .y} = get_point();
// or
let {x, y} = get_point();

// bind to same name and ascribe:
let {x: f32, y: f32} = get_point();

// function with an anonymous struct as the last argument for supporting named/default arguments
fn draw_rect<N: Float>({x: N, y: N, w: N, h: N}) {}

// calling the function
draw_rect(.x: x1: f32, .y: y1, .w: x2 - x1, .h: y2 - y1);

@phaux
Copy link
Author

phaux commented Feb 20, 2015

@CloudiDust

I just had the same idea after reading your previous post, but then I realized:

The dot before field doesn't solve the ambiguity of what is what in { a: b }. The first thing can't be anything other than a field identifier, so we don't have to make it special. The problem is that we have no way to specify that the second value is a type instead of the usual variable/subpattern.

This RFC + type ascription RFC propose only {a => b: c} for now, but we could make the {a: b} be a sugar for {a => a: b} in the future, because it would be unambiguous with {a => b}.

If this proposal doesn't get accepted, we have to specify all three values everytime. This might not be the end of the world (after all this thinking this syntax is becoming clear enough to me), but why do we want such a confusing ambiguity when we might as well just fix it before 1.0??

@CloudiDust
Copy link
Contributor

@phaux,

The dot before field doesn't solve the ambiguity of what is what in { a: b }. The first thing can't be anything other than a field identifier, so we don't have to make it special. The problem is that we have no way to specify that the second value is a type instead of the usual variable/subpattern.

I think by putting a dot before a field, it makes .a: b different from a: b, not just .a different from a, and it will solve the ambiguity. (The parser just need to search for the first . and it knows whether this is an anonymous struct, or a block).

Compared with =>, the advantage here is that .ident: is lighter-weight than ident => and definitely won't cause ambiguities with => in matches.

Also .ident: is similar enough to ident: that after the change, existing codes would look "roughly the same". (I can see some people not liking => at all, even if the "fake tuple" syntax is added, but if : is retained, prepending a . should be a change that is more acceptable.)

@phaux
Copy link
Author

phaux commented Feb 20, 2015

@CloudiDust This is even more confusing than what we have now. I don't see how these dots add any clarity over the current syntax. Here's your dot syntax example rewritten with current syntax:

let foo1 = {value: Foo}; // block
// removed dot, added comma.
// comma is currently only used for single-value tuples,
// but might be added with anonymous structs RFC:
let foo2 = {field: foo1,}; // struct

// with dots removed it's still unambiguous
let {x: point_x, y: point_y} = get_point();
let {x: point_x: f32, y: point_y: f32} = get_point();
let {x, y} = get_point();
// currently that's the only way of doing this,
// but it's still more clear than a mysterious dot before block:
let {x: x: f32, y: y: f32} = get_point();

@CloudiDust
Copy link
Contributor

@phaux without dots, what x: y means would be inconsistent (is y a type or a variable?), which is why we are having this RFC.

With dots, the rule is simple and consistent: only expr: Type/Trait (for type/trait bound specifying) and .ident: expr (for value binding) are vaild, everywhere.

That's like, with this RFC: only expr: Type/Trait and ident => expr are valid, everywhere (except in matches, where => means something else, but it may not matter).

Personally, I think let {.x: f32, .y: f32} = get_point() is better than let {x: x: f32, y: y: f32} = get_point();, especially when the field names are long.

@koeninger
Copy link

+1 to this rfc as currently written (the thing to the right of : is always a type-level identifier)

@EdorianDark
Copy link

+1, this sounds great.
But i would be surprised, if this rfc gets accepted so late in the process.

@phaux
Copy link
Author

phaux commented Feb 20, 2015

@CloudiDust When field names are long then you would want to bind to another name to shorten them anyways :)

If I was to think of a sugar for {x: x: T} I would make it something else than {.x: T}. Maybe something like {x:: T}

Here's a small comparison I wrote:

// current methods
let Rectangle{width, height, ..}: Rectangle<f32> = get_area();
let Rectangle{width, height, ..} = get_area::<Rectangle<f32>>();
// type ascription + anon structs
let {width: w: f32, height: h, ..} = get_area();
// as above + sugar (this is the syntax weirdness I mentioned in the RFC)
let {width:: f32, height, ..} = get_area();
let {.width: f32, height, ..} = get_area();
// new struct syntax (`:` is unambiguous so no need for weird syntax)
let {width: f32, height, ..} = get_area();

@CloudiDust
Copy link
Contributor

@phaux, I thought of :: too, but :: is already the namespacing operator, Even if it isn't, adding such syntax would make the usage of : more arbitary.

With the .ident: syntax: (Note: let {.x: f32, .y: f32} = get_point(); is an error, it should be let {x: f32, y: f32} = get_point();)

// If only the field name is specified, then no need for `.`
let Rectangle{width, height, ..}: Rectangle<f32> = get_area();
let Rectangle {width, height, ..} = get_area::<Rectangle<f32>>();
// type ascription + anon structs
let {.width: w: f32, .height: h, ..} = get_area();
// when specifying types only, no need to put `.` before width (I made a mistake above, this is the correct syntax)
let {width: f32, height, ..} = get_area();

@CloudiDust
Copy link
Contributor

Notice how in the above snippet, when we didn't need to rename the fields, the syntax didn't involve .s, but when we wanted to rename, the .s were there to signify which were the field names and which were the bound variables.

@CloudiDust
Copy link
Contributor

When trying to write an RFC for .ident: and comparing the current and proposed syntax in various contexts, I find that:

While .ident: is in theory more consistent, such consistency still depends on the fact that the leading . acts as a "switch", which "changes" the semantics of the : following it.

Then, if we need a "switch" anyway, why isn't the existing {} (and the :s or ,s inside it) a good one?

Which means .ident: doesn't have clear practical advantage over the current syntax.

Actually, no alternative has clear practical advantage over the current syntax.

The following examples compares the current syntax and the various proposed alternatives, with type ascription and type-inferred struct literals/named arguments implemented:

// Proposal A is the C99 designated initializer syntax.
// Proposal B is the `=>` syntax.
// Proposal C is the `.ident:` syntax.

struct Point { x: f64, y: f64 }

fn get_point() -> Point { ... }

fn use_point(p: Point) { ... }

// Point literals:
let point = Point {x: 3.0, y: 4.0} // current
let point = Point {.x = 3.0, .y = 4.0} // A
let point = Point {x => 3.0, y => 4.0} // B
let point = Point {.x: 3.0, .y: 4.0} // C

// Point patterns:
// without renaming and type ascription:
let Point {x, y} = get_point(); // current and all proposals

// with renaming but no type ascription:
let Point {x: new_x, y: new_y} = get_point(); // current
let Point {.x = new_x, .y = new_y} = get_point(); // A
let Point {x => new_x, y => new_y} = get_point(); // B
let Point {.x: new_x, .y: new_y} = get_point(); // C

// with type ascription but no renaming:
// *THIS IS THE ONLY SATUATION WHERE THE ALTERNATIVES HAVE CLEAR ADVANTAGE
// OVER THE CURRENT SYNTAX*
let Point {x: x: f64, y: y: f64} = get_point(); // current
let Point {x: f64, y: f64} = get_point(); // all proposals

// with both renaming and type ascription:
let Point {x: new_x: f64, y: new_y: f64} = get_point(); // current
let Point {.x = new_x: f64, .y = new_y: f64} = get_point(); // A
let Point {x => new_x: f64, y => new_y: f64} = get_point(); // B
let Point {.x: new_x: f64, .y: new_y: f64} = get_point(); // C

// By omitting the `Point` part above,
// we can get the corresponding type-inferred struct literal/pattern syntax.
// While in theory `{x: new_x}` is visually ambiguous,
// it can be disambiguated by the trailing `,`:
// `{x: new_x,}` is an type-inferred struct literal,
// and `{x: new_x}` is a block with type ascription.

// named arguments:

// with type-inferred struct literals:
use_point({x: 3.0, y: 4.0}); // current
use_point({.x = 3.0, .y = 4.0}); // A
use_point({x => 3.0, y => 4.0}); // B
use_point({.x: 3.0, .y: 4.0}); // C

// further sugaring: getting rid of `{}`:

// Not possible with the current syntax due to ambiguities
use_point(.x = 3.0, .y = 4.0); // A
use_point(x => 3.0, y => 4.0); // B
use_point(.x: 3.0, .y: 4.0); // C

// Note that, while the alternatives all permit us to do further sugaring,
// the results are not necessary better than `use_point({x: 3.0, y: 4.0})`.

We can see in the above examples that, while the alternatives have consistency advantages in theory, such advantages may not exactly translate into practical advantages.

@iopq
Copy link
Contributor

iopq commented Feb 21, 2015

You forgot one more example

foo.slice({ from: 1,}); //current
foo.slice({ .from = 1}); //A
foo.slice({ from => 1}); //B
foo.slice({ .from: 1}); //C

all of the proposals can disambiguate blocks from struct literals without a trailing comma, while the current syntax cannot

so if Rust gets default parameters, there will be cases where you just want to pass one member of an anonymous struct to a method and have the remaining members get default values

fn slice(&self, {
    from => start: usize = 0,
    to => end: usize = self.len()
})

this would be an example where the = syntax is not as clear because it would be:

fn slice(&self, { 
    .from = start: usize = 0,
    .to = end: usize = self.len() 
})

this is visually more confusing

@CloudiDust
Copy link
Contributor

@iopq, nice find. However, we may accept foo.slice(.from: 1) even if the current struct syntax is not changed. (Here, the . "switches", and also signifies the fact that .from is actually a field of an anonymous struct.)

if we don't change the current syntax, I'd expect the named argument declaration syntax to be:

fn slice(&self, {
    from: 0: usize,
    to: self.len(): usize
})

I see no much advantage in declaring two names for the same named parameters (from/start and to/end), if the anonymous struct is declared inline, then the field names should be used as the local variable names inside the function. We can still bind new names inside the function body if necessary.

That said, this may not be as clear as:

fn slice(&self, {
    from => 0: usize,
    to => self.len(): usize
})

or

fn slice(&self, {
    .from = 0: usize,
    .to = self.len(): usize
})

@iopq
Copy link
Contributor

iopq commented Feb 21, 2015

Rust already supports renaming struct fields:

fn foo(Bar{x: y}: Bar) -> i32 { y }

this is already valid Rust, so the slots in structs do not have to be the same as the local bindings in the function

so given anonymous structs and type ascription it would look like

fn foo({x: y: i32,}) -> i32 { y } //trailing comma may or may not be needed here

with the = syntax this would look like

fn foo({.x = y: i32}) -> i32 { y }

but if you were to add default parameters to current Rust it would look like

fn foo({x: y: i32 = 0,}) -> i32 { y }

by extension, the = with default parameters would look like

fn foo({.x = y: i32 = 0} -> i32 { y }

Although it would be weird because you'd have to pass an empty record into foo like foo({}) so I'm not sure how you'd really parse that unless there's special support for 0 argument list keyword functions and foo() just magically works

@CloudiDust
Copy link
Contributor

@iopq, I think this is a matter of perspective and perference. You prefer to combine struct definitions and patterns for convenience here, which is a valid goal. But I prefer to keep them separated, because they are separated for named structs.

The following is my understanding/perference of the named argument definition syntax.

In fn foo(Bar{x: y}: Bar) -> i32 { y }, a struct pattern is used to extract fields from a Bar argument, we are not actually defining a new struct type here.

On the other hand, anonymous structs in function signatures are struct definitions, not literals or patterns. And struct definitions don't support field renaming.

So instead of fn foo({x: y: i32,}) -> i32 { y }, the full syntax under my understanding would be:

fn foo({x: y,}: {x: i32,}) -> i32 { y }

Where {x: i32,} is defining an anonymous struct type inline, and {x: y,} is binding the value of field x to y.

And if we do not need renaming, we can omit the pattern and :, and then we have:

fn foo({x: i32,}) -> i32 { x }

{x: i32,} above is a struct definition, not a pattern, and there is no : after it. The binding of x in the function body is the result of the sugar.

And we can have default values:

fn foo({x: 42: i32,}) -> i32 { x }

(Trailing commas may or may not be needed.)

I think empty records should be {,}.

@iopq
Copy link
Contributor

iopq commented Feb 21, 2015

Whoops, I forgot to declare the type of the arguments.

Why would default values use : syntax? I think they should follow the let syntax
so

given let x: i32 = 5
therefore fn foo(x: i32 = 5) { ... } //normal default arguments
therefore fn foo(Bar{x: y: i32 = 5}: Bar) { ... } //default arguments + type ascription a struct
therefore fn foo({x: y: i32 = 5}: {x: i32}) { ... } //default arguments + type ascription + records

I seriously hope to avoid x: y: 4: i32 or whatever the order it supposed to be (I am so confused)

@CloudiDust
Copy link
Contributor

@iopq, I think by separating definitions from patterns like I proposed in the above comment, we can avoid things like x: y: 4: i32.

And the worst we have then would be x: 4: i32/x: y: i32 or x => 4: i32/x => y: i32 or .x = 4: i32/.x = y: i32, which are not that unacceptable. :)

@phaylon
Copy link

phaylon commented Feb 21, 2015

I've seen anonymous struct syntax looking like _ { ... } around a couple of times now. That would not suffer the same problems, and would seem more visibly consistent (to me).

@iopq
Copy link
Contributor

iopq commented Feb 21, 2015

@phaylon that breaks symmetry between named structs and records. We already have tuples with just ( ... ) so if I've never seen records before, but I've seen structs I'm going to try { //insert struct syntax here } and see if it compiles.

@phaux
Copy link
Author

phaux commented Feb 21, 2015

Why not just leverage existing Default trait for default arguments? We are already agreeing on structs as a substitute for named arguments. The more core Rust we reuse, the better.

Ideas (offtopic)

@phaylon
Copy link

phaylon commented Feb 21, 2015

@iopq: Well, the _ part is just about inferring the name. Personally I'm not too worried about symmetry between tuples and structs. I do however think the _ variant has more consistency between structs with and without a specified name.

@phaux
Copy link
Author

phaux commented Feb 21, 2015

@phaylon Underscore could be used to signify that you want your struct/tuple literal to coerce to a named one. Omitting _ would mean that you want your struct/tuple to stay anonymous 👍

struct Point ( i32, i32 );

let p: Point = _(1, 2); // coerces to Point (new syntax)
let p: Point = (1, 2); // error
let p: (i32, i32) = (1, 2); // fully anonymous

struct Point { x: i32, y: i32 };

let p: Point = _{x: 1, y: 2}; // coerces to Point (new syntax)
let p: Point = {x: 1, y: 2}; // error
let p: {x: i32, y: i32} = {x: 1, y: 2}; // fully anonymous (new syntax)

Look at all that symmetry!

@iopq
Copy link
Contributor

iopq commented Feb 21, 2015

@phaylon I understand this can be added to Rust:

struct Bar { a: A };
foo(_{a: b});

fn(bar: Bar) { ... }

where the name of the struct Bar is inferred, but I'm talking about anonymous structs, not inferring the type of a struct that has a definite type

@phaylon
Copy link

phaylon commented Feb 21, 2015

I'm talking about both, because just re-using blocks would make it visually very hard to find. Anonymous structs still have a name, you just can't publically refer to them.

@CloudiDust
Copy link
Contributor

I think by "an anonymous struct type", we may mean one of two things:

  • A. a struct type with a generated name that we cannot/don't usually refer to;
  • B. a structural struct type.

Note in Rust, we have anonymous tuples and named tuples (tuple structs), and anonymous tuples are structural types. So if we are to introduce anonymous struct types, I think we should introduce the "structural" one for symmetry.

However, I have realized, by utilizing Default, we don't actually need "anonymous struct types" under either interpretation, and we only need "anonymous/type-inferred struct literals/patterns"

Actually, neither interpretation A nor B helps us define named/optional/default arguments if we are to utilize Default (instead of developing another way for specifying default values):

Under Interpretation A, when we want to implement Default for an anonymous struct type, we have to refer to its generated name. Then why don't we just give it a clear name?

And Interpretation B means that something like {from: usize, to: usize} anywhere are all the same type, We can only have one implementation of Default for it. Then what should we do if different functions want different default values for from and to?

I now agree that, for anonymous/type-inferred struct literals/patterns, we should use the underscore syntax brought up by @phaylon, to disambiguate and signify that we just "don't care" about the struct names, not that the structs don't have names. (If _{x: 1} looks funny in function calls, .x: 1 is still a good sugar in my eyes.)

And what's more important (at least for this comment thread) is that the current struct syntax is fine.

@phaux phaux mentioned this pull request Feb 24, 2015
@nrc
Copy link
Member

nrc commented Feb 27, 2015

Sadly, changes to struct literal syntax at this stage are not possible. Even if there were a perfect new syntax, the level of disruption at this late stage would be hard to justify. In particular, there is not any technical clash with type ascription, only potential confusion. Since type ascription should be rare, that does not justify such a large change. There are also many people who like the current syntax (not me, actually, I much prefer =, but such is life) since it has precedence amongst other reasons, and no proposed new syntax is widely approved of and problems can be found with all of them.

@nrc nrc closed this Feb 27, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.