Named arguments increase readability a lot

About the question at the end: Optional arguments do not depend on named arguments (nor do named arguments depend on optional arguments). The two can be discussed separately without problems I think. Keeping this RFC to named arguments will make it focused and keep the discussions about it focused too.

Edit : I have programmed a medium-size project in swift that I had already done in python. Having mandatory named arguments as they are designed in Swift is very very helpful when reading code, compared to python where most arguments name can be ignored.

I miss them in Rust, that’s really my biggest pet peeves about the language.

1 Like

RFC drafts can be done by filing a PR against your own repository. Checkout a new branch with your added PR, reset master to upstream, and PR from your branch to your master. That gives you a thread on GitHub for people to give you targeted suggestions.

I think it might be better to add named arguments backwards compatibly without a new edition. My idea is to use lifetimes, because they are already used as loop labels:

fn normal(foo: bool) {}
fn named('foo: bool) {}

normal(true);
named('foo: true);

It would also be possible to rename, destructure or ignore a named argument:

fn foo(
    'ignored _: bool,
    'renamed new_name: bool,
    'destructured (a, b): (bool, bool),
) {}

foo('ignored: true, 'renamed: true, 'destructured: (true, true));

I believe that this syntax could be added both to the 2015 and 2018 edition.

P.S. I just realized that this conflicts with labelled loops, so loops would have to be wrapped in (...) for backwards compatibility:

normal('foo: loop {
    break 'foo true;
});
named('foo: (loop {
    break true;
}));
4 Likes

Perhaps a different sigil (than ') could serve the same purpose. It would be useful to find a solution that could be back-ported to the 2018 and 2015 editions.

FWIW, if we were to use 'ident, then we could swap the : with an =, and not have to special case loops:

foo('ignored = true, 'renamed = true, 'destructured = (true, true));
9 Likes

Use of =, rather than :, would also help to communicate with the reader that it's a named argument.

2 Likes

From my experience the solution with the ' sign is a problematic idea, as it is opposite to the idea of a basic principal of the proposal: Leading programmers into the right direction.

It shouldn't take extra effort to write cleaner code, but less. The code any novice programmer or code written in a hurry should already be the optimal form. Like if you don't care or don't know any better and don't type "mut" after your "let" you have already the better solution (most times).

Constants help the compiler to better optimize and are nearer to functional programming, which has a bunch of benefits with maintenance, parallel programming race conditions, etc.. But most people don't really think about that and indeed: It's more typing "work" in most languages to write a constant than a variable. And: I have seen much code where variables were used in places where a constant would've been the better choice.

From what I saw people just stick more or less to the "default". Therefore the more readable code should be the default direction the languages leads you to. It should need an extra sign to not use the default.

2 Likes

Except that the ship has already flown (if you'll forgive the mixed metaphor). The simple syntax is the positional argument syntax, and changing that would be a change on a disastrous scale.

Swift did this exact change before it committed to source-source compatibility, and changed (roughly, my Swift is very rusty at this point) func foo(arg: Arg) from meaning func foo(_ arg: Arg) to func(arg arg: Arg). This was a good change, but it meant touching every function definition to update it to the new syntax.

Rust can't do that. One of the main constraints on editions is that it should be reasonable to write code on edition N that also compiles without warnings on edition N + 1. (Also that warning-free code on edition N works as-is on edition N + 1.)

In case you misunderstood, though: if a function is called as foo('ignored = true, 'renamed = true, 'destructured = (true, true)), it would not be callable as foo(true, true, (true, true)). The names would be required. The above is about the definition site; the call site either has to be positional or has to be named arguments.

5 Likes

The RFC suggests a rustfix solution which adds an attribute, so without touching the actual source-code implementation (besides the attribute), everything would still compile fine. While "Only binary" can't work with that, Rust is, as mentioned in the RFC, still not ABI stable. So the source code has to be recompiled anyway with new versions.

I think for any progressively developed language it's impossible to not have constantly new warnings popping up in new version. Take the already mentioned `dyn Trait` Syntax for Trait Objects: Take 2 by Ixrec · Pull Request #2113 · rust-lang/rfcs · GitHub for example, it even introduced a new keyword.

1 Like

New warnings are fine. The restriction is that on any edition boundary (i.e. when you actually remove syntax) it must be able to write code that compiles without warnings before and after the edition simultaneously.

So... by editing the source code? The problem is that this touches literally every function definition. Sure, you can automate it with rustfix, but this is still a massive scale to apply a change at.

2 Likes

I agree it is a massive change, but, depending on the syntax used, it can be a very simple one.

I’m going to assume a Swift-like model will be used (whether calls are made with : or = or something else is not important here)

fn a(arg: Type): internal/external name are the same, necessary to use the name when calling.

fn b(out in: Type): different names for the caller and the implementation (out for the caller, in for the implementation).

fn c(_ in: Type): no external name, in is the name used for the implementation.

Today we write functions like a that behave like c. Changing from the current syntax to the one I presented would be the work of one big commit if rustfix provided the fix, moving all existing functions to the c format, without changing anything for callers.

The downside of this is the incompatibility between editions. A possible fix going from 2015/2018 definitions to 202x callers would be to consider all such definitions as if written in the c format. In the other direction, calling is a simple as ignoring the names of the arguments.

1 Like

Then why not make the alternatives such that a single name maintains the Rust 2015/2018 edition semantics, and the Swift-like semantics uses a different separating or prefix sigil? Rewriting what you wrote in the post above, while adding the obvious permission for both internal and external explicit names to be the same:

fn a(_ arg: Type): internal/external name are the same, necessary to use the name when calling.

fn b(out in: Type): possibly different names for the caller and the implementation (out for the caller, in for the implementation), necessary to use the external (caller) name when calling.

fn c(in: Type): no external name, in is the name used for the implementation.

I agree this is a working solution. The downside of it is: it makes it more work to use named arguments.

To me, this is opposite to what we want (increase readability) because if this increase comes with more work for the basic solution of « my argument name is the external and internal name », then the design has failed to meet its requirements.

Having to write/read more code for the better solution is only okay when there is no other option. We have one have here, of which the only downside is: it breaks existing source code when compiling to the new edition.

To this I would say: heh, this is not important. Rust will (I hope) exist for dozens of years, we shouldn’t worry about the first 6 or 7 if there is a (relatively) low hanging fruit in the design that can be improved upon, especially when the fix is quite simple as it is here.

1 Like

I don't think that the benefits outweigh the downsides. It will mean that the same code has different meanings in different editions. This will be really weird for people who contribute to Rust projects that use different editions.

Furthermore, a huge syntax change like like this will likely cause problems with version control systems. Many projects won't update to the new edition to avoid the hassle.

Then we'll have an ecosystem split, where libraries in the 202x edition that use named arguments can't be used as intended by projects that are still in the 2018 edition.

2 Likes

The downside of this is the incompatibility between editions. A possible fix going from 2015/2018 definitions to 202x callers would be to consider all such definitions as if written in the c format. In the other direction, calling is as simple as ignoring the names of the arguments.

(Italic for fixed word)

I think the solution I proposed can work and would allow interop between edition without having to rewrite every Rust function in existence. I may have missed something though and I must admit I have no idea about the feasibility of such a change in the compiler (though I would certainly try to help it were accepted).

1 Like

The problem is that if an API uses named arguments, it should always be used with named arguments. For example, using a bool as a named argument is totally fine, but when the API is used from the 2018 edition where named arguments are not available, you might have code that looks like foo(1, 5, true, false), which is incomprehensible. This would not be a problem if named arguments could be added to all editions backwards compatibly.

As people who actually get results in politics or standards have learned, compromise is the means of achieving objectives that are weighted more highly by their advocates than by the community at large.

Back in Swift 1, having a named first argument was spelled func a(#arg: Type), Rust might be able to use that.


Syntax idea:

fn a(# arg: Type) : internal/external name are the same, necessary to use the name when calling.

fn b(out in: Type) : possibly different names for the caller and the implementation ( out for the caller, in for the implementation), necessary to use the external (caller) name when calling.

fn c(_ in: Type) : no external name, in is the name used for the implementation.

fn d(in: Type) : (by edition of defining crate)

  • 2015/2018: no external name, in is the name used for the implementation.
  • 202x: internal/external name are the same, necessary to use the name when calling.

I think this syntax would allow a crate to compile with all editions without warnings by rejecting the simple spelling while transitioning, if this syntax is otherwise acceptable (I have no idea of that).

1 Like

note that fn f(out in: Type) is ambiguous with pattern matching in function arguments.

fn f(S (a, b): S) {}
//  is it an `in: Type` variant where `in` is `S(a, b)`,
// or an `out in: Type` variant where `out` is `S` and `in` is `(a, b)`?
9 Likes

Indeed, in that regard we'd have to use either the aforemention 'out in or the currently syntactically legal out @ in, which in practice is not yet usable due to it "double binding" (unless in is _; I don't think I've ever seen Rust code using arg @ _: ty syntax out there).