[Pre-RFC] Named .arguments

This is a new proposal to introduce named arguments a.k.a. keyword arguments in Rust. While there have been multiple proposals for this (e.g. this one), this RFC uses a different syntax that doesn't conflict with existing and planned syntax such as type ascription.

EDIT: I changed the syntax from 'arg to .arg.

EDIT 2: The RFC is now submitted, view it here. Note that I updated quite a few sections; argument names are no longer mandatory in function calls.

:framed_picture: Rendered

11 Likes

I'm worried that repurposing 'foo for something other than lifetimes could be confusing.

I don't think it's necessary to have special support for renaming arguments in this way. That example could be rewritten as

fn foo('a: u32) {
   let b = a;
   ...
}

which would simplify the grammar.

8 Likes

An idea for an alternative: use the pub keyword to indicate that the argument name is a 'public' part of the function signature:

fn foo(arg: u8) {} // Invoked as foo(0): the name 'arg' is an implemenation detail, and cannot be specified by callers
fn bar(pub arg: u8) {} // invoked as bar(arg = 0): the name 'arg' is a public part of the signature.

However, this might lead users to think that the argument itself is somehow public/private, rather than the name.

2 Likes

Rust is already perceived as a sigil heavy language and re-using ' for public arguments will only make situation worse. My previous proposal was using pub, although not for each separate argument, but to denote a "public argument block".

4 Likes

That was proposed in a previous RFC. I used the ' syntax in function definitions because I also use it in function calls.

Thanks - I should have checked the history of this feature before posting :smile:

My bad, I should have added the link to the post.

The main purpose of this is pattern matching. For example, you can destructure a tuple there, or when implementing a trait method with named arguments, you can ignore some of them with 'arg _. It's not necessary, but more convenient.

One problem I see with this is that it includes an avoidable issue shared by Swift's named arguments, which they have discussed the possibility of fixing (source incompatibly) here. The issue is that this example:

would make adding "function overloading" / (considering argument names as part of the function name) API breaking.

// API v1.0.0
fn foo('a: i32, 'b: i32) {}

// user
let f: fn(i32, i32) = foo; // <-- this line
f(4, 2); // okay
// API v2.0.0 !
fn foo('a: i32, 'b: i32) {…}
fn foo('c: i32, 'd: i32) {…}

// user upgrades:
let f: fn(i32, i32) = foo; // Error: ambiguous use of `foo`
f(4, 2); // okay

It would be better to require all argument names from the outset:

// API v1.0.0
fn foo('a: i32, 'b: i32) {}

// user
let f: fn(i32, i32) = foo('a, 'b); // <-- syntax to be bikesheded
f(4, 2); // okay
1 Like

I'm not a fan of function overloading. I mentioned it in the future possibilities only because it is tangentially related, but I don't think that it should be added to Rust.

It's not necessarily overloading if the argument labels are considered part of the function name. For example, if

fn foo('bar x: i32) -> i32 { ... }

could be referenced as

let f = foo'bar;

Or even

let f'b: fn(i32) -> i32 = foo'bar;
let x = f('b = 5);

then a function named just foo just wouldn't have the same name as a function named foo'bar.

4 Likes

Type ascription is not stable, in part because of this grammatical conflict. It is feasible to carve out identifier : in these positions as resolving to a named argument instead of type ascription. If you want type ascription, you would have to write e.g. foo((a: b)); however, it should be rare to want to use type ascription directly on a variable in the first place, since you could normally just put the type in the variable declaration.

(Alternately, there's always the possibility of just removing type ascription.)

I think this syntax would be vastly preferable due to consistency with the rest of the language.

3 Likes

<ident>: <expr> is used only in structs and <pat>: <type> is more prevalent. So if named arguments will not be tied to structs (like in this proposal or in the pub ones), then I think it will be a false consistency, which should not be followed.

5 Likes

Type ascription is useful for picking or coercing a type for a generic function.

let a = &b"1234";
f(a: &[u8])  // instead of `f(a as &[u8])` or `f::<[u8]>(a)`

I don't see any part in Rust supporting named arguments using :. The closest match is println!("{foo}", foo=4) which uses =.

3 Likes

Or just let a: &[u8] = b"1234". Because it's a variable, there's somewhere else to put the type, which would be more idiomatic in any case.

I think named arguments are very analogous to structs even if they aren't structs themselves.

Also, the alternative syntax suggested in the OP has the severe downside of looking like lifetimes while being completely unrelated. Both lifetimes and (independently) the syntax Rust uses for lifetimes have a reputation for being confusing to newcomers; no need to further conflate things. Beyond name: value, other familiar-seeming options are name => value and (notwithstanding the OP's valid points about editions) name = value, though, again, I think the inconsistency with structs makes them worse than name: value.

2 Likes

but perhaps i do want to retain the more specific type a: &[u8; 4].

or maybe a is not a local variable, but a function argument or static variable which you can't control.

you also can't just "carve out" $ident : and keep the remaining $expr : for type ascription use, since it makes f(a: T) and f(&b: T) having different semantics, which violates the consistency even worse.

in any case i don't support sacrificing type ascription for named arguments.

4 Likes

I like the idea in general, but syntactical association with lifetimes is problematic. Lifetimes are the scariest most confusing part of the language, so I don't think Rust can afford reusing that syntax for something else.

Maybe a different sigil?

foo(@c = 5, @b = 3, @a = 1);
foo($c = 5, $b = 3, $a = 1);
foo(^c = 5, ^b = 3, ^a = 1);
foo(#c = 5, #b = 3, #a = 1);
foo(.c = 5, .b = 3, .a = 1);
6 Likes

I like the leading point (.) because, for me, it implies that the following parameter name is interpreted with respect to the called function name. It's clearly not method syntax, but for me it does have a somewhat-similar mental model of name scope.

11 Likes

On a related note I'm worried that this will make the visual ambiguity between lifetimes and character constants worse. Right now, lifetime 'a is only used inside a type parameter list <...>, and character constant 'a' never appears in that context. But with this change, now we can have either of them inside function argument lists. I trust that the parser can handle it, but for people reading unfamiliar code,

foo('a = 23)

is going to cause a mental hiccup as they try to figure out whether that wasn't supposed to be

foo('a', 23)

or something like that. And vice versa.

You're right, that could also cause problems with autocomplete (e.g. when typing ' and the IDE adds another ').

I like the idea of using .arg, since the dot is quite unobtrusive (#arg would add more visual clutter). Please tell me what you think, so I can update the proposal.

2 Likes