Struct sugar

One idea to support overloading is to add similar sugar to enums.

Enum variants can not have defaults, because they use the same syntax as for struct tuples.

Functions, that are overloaded, can be called with named argument syntax as long at least one of the names is not an argument to the other overloaded functions.

All enum variants except one must take parameters, where the parameters are of different type signatures. The one without parameters represents the unit type (). There must exist one overloaded function for each enum.

// With overloading
fn foo(x: uint) { ... }
fn foo(y: f64) { ... }
foo(3);
foo(3.0);

// With enum sugar
enum Foo { X(uint), Y(f64) }
fn foo(X(x)) { ... }
fn foo(Y(y)) { ... }
foo(3); // Desugars to foo(X(uint))
foo(3.0); // Desugars to foo(Y(f64))

The enum type can be passed as argument to the function:

let x = X(2);
foo(x);

When it is not specified a type, it desugars at the function call. This is because a function call should not interfere with type interference.

let x = 2;
foo(x);

// Desugars to

let x = 2;
foo(X(x));

The compiler might take advantage of knowing the variant to do optimizations.

A common pattern for overloading sugar will be with Option.

With overloading

fn foo(x: uint) { ... }
fn foo() { ... }
foo(2);
foo();

With enum sugar

fn foo(Some(x: uint)) { ... }
fn foo(None) { ... }

foo(Some(2));
foo(2); // Desugars to Some(2)
foo(); // Desugars to None
foo(None);

let x = 3;
foo(x);

// Desugars to

let x = 3;
foo(Some(x));

When specifying the type Option, desugaring happens at assignment:

let x: Option<uint> = 3;
foo(x);

// Desugars to

let x: Option<uint> = Some(3);
foo(x);

Summary

  • A sugar for struct syntax that allows named arguments and default values
  • Can be extended to enums, which allows overloading
  • Function call syntax reflects sugar to allow refactoring without breaking code

The sugar is valid when:

  • There is a single argument to a function, or, there is a single argument besides self in a method.
  • All members of a struct is public, or, function name is overloaded when taking enum.

Table overview:

|-------------|---------------------|----------------------------|
| Fn \ Syntax | Named               | Unnamed                    |
|-------------|---------------------|----------------------------|
| Unique name | struct/syntax       | struct/syntax              |
| Overloaded  | enum(struct)/syntax | enum/enum(struct)/syntax   |
  • A colon pub: struct makes all members in struct public
  • Desugar x: 1, y into Foo { x: 1, y: y } when Foo is expected, at least one named argument is required
  • Add new trait trait Optional<T> { fn none() -> Self, fn some(T) -> Self, fn to_option(self) -> Option<T> } and implement it for Option
  • Desugar val into Optional::some(val) whenever val is T and U: Optional<T> is expected where T and U do not match
  • Fill in with Optional::none() annotated x, .. with no following value
  • Desugar foo(bar: T = expr) { ... } into foo(bar: T) { let bar = match bar.to_option() { None => { expr } Some(val) => val } }; ...}
  • Desugar function argument and destructuring Foo { bar = expr } same as previous point
  • Type annotation becomes optional for destructure pattern in function argument, unless a generic or lifetime parameter is required
  • Enums used for overloading must have variants with unique parameter type signature
  • Desugar values into enum variants when function name is overloaded

Effects of this RFC:

  • Better ergonomics
  • Avoid line noise that do not represent a choice of performance
  • Cleaner syntax for unwrapping values from structs
  • Non-ambigious named syntax, overloading and defaults

It seems necessary to allow a struct tuple constructor on structs if we implement overloading. Here is the reason:

When refactoring an overloaded function that one wants to call with named parameters, one can combine an enum that wraps a structure, for example:

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

// Works with overloading and named argument syntax
foo.slice();
foo.slice(0, 10);
foo.slice(start: 0, ..);

When we refactor, we have to pick either unnamed or named argument syntax, because struct tuples only support unnamed while structs only support named.

struct Range { from: Option<uint>, to: Option<uint> }
fn slice(&self, Some(Range { from = 0, to = self.len() }) { ... }
fn slice(&self, None) { ... }

foo.slice(); // Works as before
foo.slice(0, 10); // ERROR: This function uses named arguments
// Desugars into Some(Range { from: Some(0), to: None })
foo.slice(start: 0, ..);

We need the tuple constructor for structs to avoid error. This could be implemented manually by writing a function Foo that constructs Foo.

When wrapping a struct in an enum variant:

struct UpdateArgs { dt: f64 }
enum { Update(UpdateArgs), Render(RenderArgs) }

It is possible to call an overloaded function with named syntax in two ways:

fn foo(Update(UpdateArgs { dt })) { ... }
fn foo(Render(RenderArgs { ... })) { ... }

foo(Update(dt: dt)) // Desugars struct
foo(UpdateArgs { dt: dt }) // Desugars enum

To avoid ambiguity, the only case where desugar of both enum and struct happens is when Option wraps a non-Option type.

fn foo(Some(UpdateArgs { dt })) { ... }
fn foo(None) { ... }
foo(dt: 0.5) // Desugars both enum and struct

One can use overloading to get rid of the ā€˜ā€¦ā€™ syntax for named arguments:

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

foo.slice(from: 0, to: 10);
foo.slice(from: 0);

One alternative is to require named calling syntax on overloaded functions. This will force the programmer to refactor into structs instead of struct tuples.

It might be possible to allow overloading with a mixture of enum variants and function arguments. It works when enum variants have different type signatures and the signatures of normal functions are treated separately from the enums.

We can choose to go with:

  • Only struct sugar, which will enable defaults
  • Only enum sugar, which will enable overloading
  • Both struct and enum sugar, which will enable both

In my opinion I think we should drop enum sugar because of the trait system. It makes it harder to look up in the documentation.

I think that overriding the tuple syntax might not be a bad idea after all. When having a vector of structs, it could be nice to write it like tuples:

// Struct sugar
let foo: Vec<Foo> = vec![
    { first_name: "John", last_name: "Red" },
    { first_name: "Donald", last_name: "Green" },
    { first_name: "Mike", last_name: "Orange" }
]

// Tuple sugar for structs
let foo: Vec<Foo> = vec![
    ("John", "Red"),
    ("Donald", "Green"),
    ("Mike", "Orange")
]

This also makes it possible to call a function that takes a struct as a normal function.

The tuple sugar can be an implicit cast into the struct, but only if all members of the struct are public. The same goes for implicit cast into the tuple struct.

Struct:

  • x: val1, y: val2 =desugar> Foo { x: val1, y: val2 }
  • { x: val1, y: val2 } =desugar> Foo { x: val1, y: val2 }
  • (val1, val2) =implicit cast> Foo { x: val1, y: val2 }

When only brackets are specified, it counts as a block, so this does not actually count as desugar.

Struct tuple:

(val1, val2) =implicit cast> Foo(val1, val2)

Using structs to fake keyword arguments is a giant hack (it requires unergonomic declaration and use of an argument struct). I’d rather keep using a macro for that use case even if this sugar was implemented.

Not necessarily, read rest of the comments.

I don't know what you're referring to. The totality of your comments deal with the call site, while I am talking about using struct declaration and use. Any struct syntax sugar will require this preamble and usage:

struct FuncArgs {
    arg1: int = 1,
    arg2: int = 2,
    arg3: int = 3,
}

fn func(args: FuncArgs) -> int {
    args.arg1 + args.arg2 + args.arg3
}

vs

fn func(arg1: int = 1, arg2: int = 2, arg3: int = 3) -> int {
    arg1 + arg2 + arg3
}

One looks like a proper feature, and one looks like a hack.

1 Like

The idea is to support both, allow refactoring into a struct when you need it. This is probably how I would write it:

fn func(FuncArgs { arg1, arg2, arg2 }) -> int {
    arg1 + arg2 + arg3
}

What I like in this proposal is that it’s not adding any features to Rust except of sugar (which you don’t have to use) that compiles to currently legal Rust code. It is kind of hack, butit’s not uncommon to pass a structure as an argument, when they are complicated enough. Another advantage of this approach (in comparison to keyword arguments as a language feature) is that it’s always simply a struct: it’s obvious when it’s evaluated and how it is passed to callee; and you enable caller to store that struct anywhere and pass it as a single value.

Unergonomic declaration isn’t that bad, because you delcare function only once, but it can be sugared too, or at least wrapped behind a simple macro too, so I don’t think is a problem.

One think I don’t agree with @bvssvni is making None special. While it may save few keystrokes, i think it adds really a lot complexity and it’s not obvious.

I guess that not really, you would either have to call foo((1,2)) or introduce ambiguity. Moreover, it would require introducing either implicit casts from tuples or fallback to tuples, for that to work:

let arg = (1,2); // treat it as truct literal here
foo(arg); // or implicitely cast here?

similarly as following would:

let arg = x: 1, y: 2; // obvious that it's a struct
foo(arg);

Anyway, I think one of the reasons why Rust currently requires field names in struct declaration is to make it obvious without consulting struct definition and being immune to reordering of fields. Let's keep it that way and let tuples stay tuples.

You can’t fix every ergonomic issue with a macro. For one, the documentation of the field names of this argument struct will be separate from the documentation function that takes those arguments. The languages that encourage passing structs when there are too many arguments are also languages without keyword arguments, so I reject that line of reasoning (at least, I can’t think of an API in Python that does this instead of using keyword arguments). I really don’t see what is gained by convoluting the otherwise interesting feature of default member values for something that it is very ill suited for. In particular, I still find that all of the proposed syntaxes are inferior to the procedural macro I made to solve this problem.

Additionaly, this duality is similar to struct variants and tuple variants with a struct value. I find them both useful at different times.

We should make struct sugar as good as possible before we decide whether to put it in Rust. I think the summary is based on results from the analysis, but there could be more analysis to improve it. What we want is as few rules as possible that cover all cases.

Instead of implicit casting from tuples, we can desugar

x, y

into

Foo { x: x, y: y }

This preserves ordering and requires the names to match. It will give errors when names do not match if the function takes a struct, but not for normal funtion arguments.

When refactoring, you always use a named struct, so we can drop struct tuples from the RFC.