Struct sugar

I’d like you to consider two ideas for making it easier to work with structs. I don’t know if these ideas are utterly crazy:

  • Sugar for default field values.
  • Implicit struct instantiation as function argument.

This is what I would like to be able to write:

struct A {x: int = 100, y: int = 1000}

fn main() {
    foo(x:12);
}

fn foo(a: A) {
    println!("{} {}", a.x, a.y);
}

This is what you would have to write today:

use std::default::Default;

struct A {x: int, y: int}

impl Default for A {
    fn default () -> A {
        A {x: 100, y: 1000}
    }
}

fn main() {
    foo(A{x:12, ..Default::default()});
}

fn foo(a: A) {
    println!("{} {}", a.x, a.y);
}

So, basically specify the default values in the struct definition instead of using a Default trait. And, when calling a function that takes a struct leave out the struct name (known at compile-time anyhow from the function signature) and the also leave out brackets.

A function that borrows a struct would probably do so implicitly from the call point. Does it matter? It’s just an r-value.

5 Likes

I would prefer to see something like this rather than incorporating keyword arguments into the function declaration syntax.

I had previously put up informal suggestions for ways to:

We even went so far as to discuss default args and keyword values in a weekly meeting a year ago (holy cow, a year ago…) : https://github.com/rust-lang/meeting-minutes/blob/master/weekly-meetings/2013-09-17.md#number-6973--default-arguments-and-keyword-values

Not sure if any of this is actually helpful, but maybe seeing some of the conversations in context can help you get an idea of how receptive people might be.

Encouraging to see so much interest in and support for these sorts of ideas!

In the proposal above, the implicit instantiation would only be applied when the struct is the only formal parameter. I suppose that this would simplify the rules a lot, so as to not having to get tangled up with overloading. I believe this to be completely unambiguous.

I really like the idea of using just structs and syntax sugar for keyword arguments! Even wanted to propose same thing.

I think that this sugar it could be extended to be used in any expression of already inferred type, not only in function arguments. For example:

struct Point {x: int, y: int};
fn here() -> Point { x: 1, y: 2 }

let v: Vec<Point> = Vec::new();
let p = x: 2, y: 4;
v.push(p);

Surrounding fields with {} could be possible too, and necessary for nesting:

struct Line {a: Point, b: Point}
fn fromZero(p: Point) -> Line { a: {x: 0, y: 0}, b: p }

I think it wouldn’t be ambigous and hard to parse and it would make it easier to DRY. What do you think?

Edit: It could work in patterns too (with obligatory {}).

fn here() -> Point { x: 1, y: 2 }

This looks awesome, but I’m not sure it’s entirely unproblematic. Can x be interpreted as a label?

Label names have to start with apostrophe (as in lifetimes). But even if Rust would get rid of apostrophes, that wouldn’t create ambiguity, because label has to be followed by loop, while or for.

Changing : to = in struct declaration, as it was proposed, will break everything though.

A side benefit of this would be being able to specify defaults for only part of a struct, while requiring the rest to be specified at instantiation.

This looks very ergonomic. I would like to see this idea pursued further. I have lots of places where this could make the code shorter.

I want to share some more thoughts: I think we want to be able to differentiate between using defaults on purpose and just instatiating a struct which happens to have a default. For example, let’s say Vec3 has a default, and you are passing it to a function:

foo(x: 1, z: 3); // forgotten y

So I propose to add an obligatory .. at the end, if your intention is to use defaults:

bar(param: 42, ..); // which would desugar into
bar(ParamStruct{param: 42, ..Default::default()});

Furthermore, we can add an struct attribute forbiding usage of such a struct without ... That would allow to create apis which can add fields to struct without breaking backward compatibility.

1 Like

For safety, ‘…’ could be required unless you specify the attribute.

This makes a pattern nicer where enum variants wraps a struct:

Update(dt: self.dt);

vs

Update(UpdateArgs { dt: self.dt });

When matching:

match e {
    Update(dt: dt) => { ... }
}

With if let syntax:

if let Update(dt: dt) = e {
    ...
}

I don’t think that more than one .. per struct would be necessary, though. Also, default values are supposed to be safe and unsurprising. So, I’m not sure the .. marker is really needed.

Would like to see this idea developed into an RFC. If this will have huge impact on the syntax, perhaps the RFC will be too much work for one person? How can we progress on the idea?

There’s a small point of inefficiency in the current usage of Default; with default fields the compiler should be compiling to a set of steps like:

  • Create an instance of A with field x as the specified one and the rest of the fields as [default options here]

it currently compiles to

  • Call the default() method, store its result in a temp var
    • Go up the call stack (Can be removed with an #[inline])
    • Create an instance of A where all fields are the default
    • Return it (going down the call stack)
  • Now, set the value of the field x to the specified value

Aside from the extra fiddling that the method call brings in, here we’re assigning to x twice. Which could be a heavy operation if x was something other than a number.

IMO, sugars shouldn’t be less performant than their expanded forms.

I like the proposed sugar, though I’m not too fond of theproposal for kwargs – what happens if there are two arguments?

In cases where non-static expressions are needed, we could have some sugar for Option:

pub struct SliceArgs {
     pub from: Option<uint>,
     pub to: Option<uint>,
 }

 pub fn slice(&self, SliceArgs { from = 0, to = self.len() }) {
     ...
 }

 foo.slice(..);
 foo.slice(from: start, ..);
 foo.slice(to: end, ..);
 foo.slice(from: start, to: end);
1 Like

It seems that sugaring for Default should be avoided of performance concerns and the disagreement of a proper default value.

Structs should simply allow default values.

To simplify parsing rules and allow macros where new struct members are intended withought breaking, the ‘, …’ at the end should be allowed even there are no members to initialize.

The parsing rules where ‘[]’ means optional:

[Foo] [{] (member: value),* [,] [..] [}]
  • If the ‘{’ bracket is specified, it must be closed by ‘}’
  • If a member is specified, a trailing comma is required
  • If no member is specified, there should be no comma

One idea is to support unit structs in the same syntax, by allowing structs to be initialized without brackets:

..
UnitFoo
UnitFoo ..
Foo ..
Foo x: val, ..

If we go this route, I think ‘…’ should be mandatory whenever default values are used. The reasons are

  1. to let the programmer control intention
  2. provide unambiguous grammar when reading code.

Option sugar can be added to simplify destructuring of members that are optional. This will also be used in function arguments to evaluate non-static expressions.

For non-static expressions left expressions, where ‘[]’ is optional, the parsing rules are:

Foo { ([member:] [ref] [mut] var [= empty_value]),+ , [..] }
  • The type of the member must be an enum of type Option
  • The struct name and brackets are mandatory
  • Member is optional only if the variable name matches member name
  • At least one member must be specified

The type must be Option because it needs to …:

  • … be an enum
  • … be unwrappable
  • … have an empty variant
  • … be declared in the struct (Result is not suitable)

Since there is only one value to unwrap, this leaves us with Option as the only candidate.

// No unwrap
let Foo {x, ..} = foo;

// Unwrap with default
let Foo {x = 3, ..} = foo;

The reason struct name is mandatory is because the compiler would not be able to infer the type otherwise. An alternative syntax is to leave out the struct name but require a type annotation.

Whenever the left argument type is Option, a right argument expression which type is not Option desugars to ‘Some(value)’.

let x: Option<uint> = 3;

When a struct takes no generic arguments, the type annotation is optional.

struct Bar { x: Option<uint> }

fn foo(Bar { x }) { ... } // No unwrap
fn foo(Bar { x = 3 }) { ... } // Unwrap with default

foo(x: 2)
foo(x: Some(2))

Option is the only type with a fixed default value, which is None. It is not possible to override this in struct definition. This is to avoid ambiguity.

struct Bar { x: Option<uint>, y: Option<uint> }

fn foo(Bar { x = 3, .. }) { ... } // Ignore y
fn foo(Bar { x = 3, y }) { ... }

When taking a struct by reference, the type annotation is required.

fn foo(&self, &Bar { ref mut x = 3, ref y }: &mut Bar) { ... }

Non-static expressions when unwrapping by default in functions, but must depend only on arguments before. This is to avoid cyclic dependencies.

// y depends on x
fn foo(Bar { x = 3, y = x + 2 }) { ... }

// y depends on self
fn foo(&self, Bar { x = 0, y = self.len() } { ... } 

A non-static expression can use brackets to execute arbitrary code.

You should be able to return in the expression when it is None:

let Foo { x = return } = foo;

This is equivalent to:

let Foo { x } = foo;
let x = match x { None => return, Some(x) => x };

With this implemented, it will make sense to support named arguments and optional arguments with default.

// Unwraps and sets default
fn foo(x: uint = 3) { ... }

foo(..)
foo(4)

The syntax for calling is the same as for struct sugar. If one argument is named, then all other arguments must be named or use defaults. When using defaults, ‘, …’ must be added.

Option types are not allowed to have default argument value. It makes no sense to default them, since it defaults to None, and if you default to something else you do not need Option.

A similar sugar can be applied to struct tuples. This makes it easier to refactor struct tuples into structs and use them as arguments.

// Struct tuple syntax
let foo = Foo(x, y);

// Struct tuple sugar
let foo: Foo = x, y;

Unlike structs, it is not allowed to use parenthesis, because these are reserved for tuples. Mixing them would be confusing. This can be solved by using implicit casting from tuples to struct and struct tuples.

Struct tuples can be used in function arguments, but can only be desugared if it is the only argument beside ‘self’. Like structs, they do not need a type annotation if the type takes no generic arguments.

fn foo(&self, Foo(x, y = 0)) { ... }

foo(2, 3);
foo(2, ..);

Like structs, non-static expressions can be used as defaults in left arguments:

let Foo(x = 3, y) = foo; // Unwrap x with default

Like structs, return can be called in default expression.

Like structs, default expressions can depend on variables in scope and previous destructured values.

Unlike structs, struct tuples do not allow optional arguments. A value can be desugared to ‘Some(value)’. If one wishes to not specify a value, or one can type ‘None’.