Named arguments increase readability a lot

The topic came up more than once, but always died: Named arguments.

I used them in Objective-C and Swift for years, and while I consider Objective-C to be a really bad designed language, I always appreciated the named arguments. Source code is more read than written, and named, fixed order (!), arguments increase the code readability a lot!

Example in Swift: company.build(house, from: dateStart, to: dateStop, afterThat: whitePaint, finally: sell)

I bet there are much better examples, but every time I program in another language I miss the named arguments. Everything else is in comparison mostly a flavor difference (traits vs OOP, etc.). You shouldn't rely on an IDE to find out what each argument is. It should be understood directly from the source code.

My (non-script) languages I programmed really a lot in, are so far: Java, C#, Objective-C, Swift, C/C++.

My question: Has anyone who programmed a lot in Swift/Objective-C and then later in another language not missed especially this language feature?

7 Likes

I can't speak specifically about veterans of Swift/ObjC, but requests for named parameters in Rust are certainly nothing new, and there's been loads of detailed discussion in the past. The best place to start is probably:

If the question here is "why haven't we done this yet?" then my personal impression is that the answer is a combination of:

  • options objects and using enums in place of bools/ints/etc are already good enough for many of the use cases, so the motivation is simply not as pressing as e.g. async/await or module system reform or NLL
  • typical named arguments proposals have very heavy overlap if not direct conflicts with other proposals, including:
    • optional arguments
    • type ascription, since : is the obvious sigil for both
    • structural records / anonymous structs, since they'd make options objects even more ergonomic, likely covering all of the motivation for named args
  • named/keyword arguments have subtle semver implications, which other languages have suffered from in practice, so even without the conflicting proposals there's some serious design work to do here
  • For the last few years the Rust project as a whole has been carrying an unhealthily large backlog of features that have achieved community consensus in an RFC, but have yet to be fully implemented and stabilized. Untangling the keyword/optional/named arguments mess simply isn't one of those features with an existing consensus; we need to focus our resources on shipping all the things we've already designed.
34 Likes

Kotlin also has named arguments. I'd really like to see them implemented in Rust!

3 Likes

The problem with named arguments is that if you implement it, for example, like it's done in Swift, is touching the core of the language. The further this is delayed it will be harder to implement later.

Like: Much harder, since it has the potential to touch every function call.

Async/await and others in opposite are only relevant for some special situations. Other solutions like structural records are only syntax sugar, but nothing like fixed order, named arguments which enforce you to write readable code. Sure nothing stops you from writing company.build(house, a: v, a2: v2, a3: v3, a4: v4), but in practice people tend to go the route the leading examples entice them to.

I can't imagine after structural records got implemented the official "Coding Guideline" of Rust would be "Better use structural records for every function call".

Direct, fixed order, named arguments instead make the code actually more readable, far less error prone, not just some simple syntax sugar, like optional arguments, etc..

I think this should top the syntax sugar features, shouldn't it?

2 Likes

Why do you think that? Named arguments aren't in conflict with any other language feature, as far as I can tell.

In some cases they do, but it is not certain. If you're using a single function with many parameters, it helps. But so does pulling up the docs for that function. You still have to decide if you want a single function with many parameters or a few small functions with fewer parameters. In fact, in many cases, what you really want is a module, not a function.

std::fs::OpenOptions is not particularly unreadable.

Capability-wise, named arguments are inferior to the builder pattern because you can't leverage the type system to enforce mutually exclusive arguments.

You also risk introducing API breakage when you change an argument's name.

This is not true in Swift and ObjC. They don't have optional arguments. Their named arguments are just a clever syntactical interleaving of function name.

createFileAtPath(p, withMode: m) is implemented by calling something like createFileAtPath_withMode(p, m) — just a regular function with two required arguments.

You can have multiple of these, and that's not even function overloading. These are just different function names.

It works really well.

8 Likes

As a reminder, if you use an IDE like VSCode + rust-analyzer, you get inline hints that look like this:

image

Which looks a bit like your Swift example. (If you're not familiar with this style of inline hints, the grey text is not text in my source code, but it's text that the editor is displaying automatically).

11 Likes

Without the Swift/Objective-C pattern you would name your arguments completely different.

The naming pattern in Swift builds complete keyword sentences that basically tell a story (you can have separate names for your method-local variables if e.g. "afterThat" is a too obscure name). That is just good self-explanatory code.

From practice I know that many projects have no real documentation, where Swift/Objective-C projects have still the best "In-Code" documentation. I know docs should always exist, but a look in the real world tells the opposite. That's why some people dumped C++ in favor of Rust, because "C++ is perfect, you can write bad code in any language" wasn't good enough.

Good, useful Open Source Java libraries haven often no good "Per-Method" documentation, internal functions have rarely any documentation and the library just a good "General" documentation. But they have very "Speaking" method names, which is very useful.

Now imagine if they had named parameters and every method told a story...

I want to reiterate: Should a language rely on an IDE to make it good readable?! And even then: JetBrains many IDEs had this feature for a long time, and I switched it off, because it was too irritating for me when the argument names appear/disappear and the cursor jumps back and forth.

Such crutches show also language weaknesses, in an ideal world they wouldn't exist.

Is there any chance that this feature could be added by a sponsorship from Google, Microsoft, etc?

4 Likes

Objective-C and even Swift have significantly weaker type sytems than that of Rust, though. In Rust, self-documenting code is usually created by means of the type system. I have used Objective-C and Swift for several years (I've been following Swift since its public release and I have been programming Objective-C since 2009), but I never particularly missed named arguments in Rust even after having used them extensively in the other two languages.

Furthermore, named arguments have come up repeatedly in the past, but somehow, proposals invariably seem to have ignored important details which are a pain to flesh out. For example, how indirect calls with named arguments should be typechecked, i.e. whether the argument name is part of the function signature/type. These questions are not trivial, so it's not merely a matter of "just implementing it".

4 Likes

This is a good question. I don't know the full answer, but I think in general the answer is "no". But programming languages don't exist in a vacuum, but instead they live in a complex ecosystem along side their tooling, libraries, community, and IDEs. Anyway, I just wanted to point out this feature, because I don't think rust will gain named arguments in the near future, and using IntelliJ or VSCode to get something similar is a thing that is possible today (if that's what one wants).

7 Likes

I would be more concerned about whether this feature even makes Rust more readable. Most of the complaints I've heard about Rust's syntax seem to be based on the fact that there is a high density of symbols and context to track, and adding yet another optional syntax and context to be aware of when reading a function invocation or definition is not necessarily helping.

I'm certainly in favor of named arguments, but I've internalized most of Rust's syntax pretty well. I'm not sure a beginner is going to find Point { x: 0.0, y: 0.0 } and point(x=0.0, y=0.0) different enough to quickly identify the difference in semantics. Only one can be use in pattern matching, for instance. But maybe it doesn't matter.

3 Likes

To be clear, any good keyword arguments feature wouldn't be optional. Every argument either is a positional argument or a (required) keyword argument.

However, it would definitely be a major change, insomuch as it would change the best way to design APIs, to take advantage of the fluency that kwargs give.

To be completely honest, I feel "second class" keyword arguments via anonymous records fit Rust better at this point. If we were pre-1.0 and had carte blanche to just rewrite the entire stdlib to use Swift-style kwargs, I would (probably) support that, because they do help quite a lot for obvious API naming.

But where we are, with a stable language, any kwargs feature is going to feel bolted on, because the majority of the stdlib doesn't use it. Anonymous records (and (almost) equivalently (for API design), struct literals without naming the type), on the other hand, "just" make the option struct pattern easier to use.

12 Likes

By 'optional syntax', I meant a syntax that you are not required to use.

Define "good". I agree that in the abstract, Swift-style mandatory keyword arguments are better than Python-style optional ones. But just as you say, adding mandatory keyword arguments to Rust now would bifurcate API design between APIs that use them and ones that don't, making it probably a bad idea. Optional keyword arguments wouldn't have that problem, and in my opinion they would still improve the language while being more broadly applicable than anonymous records.

1 Like

Keyword arguments would at least need to be optin at the declaration site, to avoid argument names implicitly becoming part of the semver guarantee.

5 Likes

If keyword arguments are added (which I loved in Swift and miss a lot in Rust, this was a very big pain point for me when starting), We could do it like this:

Edition 2015,2018,2021?: no need for them Edition 2021?,2024: they are mandatory.

That way we can start working on the std and with big crates authors to prepare for it.

I like the way Swift does it with public/private name and _ for the external name when you just want with_capacity(cap) instead of with_capacity(capacity: cap).

About semver: with the separation between public/private keywords args the pain would be reduced (I think so, I have no evidence about it) and today we already have to think about method name, which is often way way harder than arguments (it was for me in Swift, since the keyword are often logical when you have the function name).

I don’t think keywords args should be used in method recognition, they should be syntactic sugar for human (I find MongoClient::new(address: "localhost", user: "default", password: "default") way clearer than MongoClient::new("localhost", "default", "default")) but I think it should not be possible to create Vec::new(withCapacity:) and Vec::new(fromSlice:), because a new method is often clearer than two methods with differing arguments and it helps a lot when searching the documentation (not seeing 10 methods named new for example).

2 Likes

What does it mean for keyword args to be "mandatory" if they aren't part of the function's type? Especially in generic code where the type is all you have.

2 Likes

By generic code I think you mean closures ?

In this particular situation mandatory would be « they must be present when the method called is uniquely identifiable ». In the following example the cb closure is not uniquely identifiable because the caller can pass any closure matching the types, regardless of the argument names, and so the original name in callback is not used.

If so here is how I imagine it:

fn fn_with_callback<F: FnOnce(name: A) -> B>(atEnd cb: F) -> Result<B, Error> {
    /* ... */
    Ok(cb(name: value_of_type_A))
}

fn callback(end_value: A) -> B {
    /* ... */
}

fn_with_callback(atEnd: callback);

It means adding the possibility of name to Fn, FnOnce and FnMut (and possibly other types I don’t remember/know of), and I have no idea about the feasibility of that.

Parameter names would be overridden by the type of the cb closure (possibly removing them if the type is Fn(A) -> B).

1 Like

Yes, and that is needed because it is difficult to understand reliably functions with more that 1 or 2 parameters (beyond the receiver) based on types alone. When you look at the source code in things that aren't IDE's you don't have that help. If this were in Rust, and it were required of any function with more than 2 or 3 parameters, you wouldn't need that feature in IDE's and the same information would be there no matter how you were looking at the code.

In other words, I don't see the fact that IDE's implement the feature you showed as being something that justifies not having this in the language. In fact, I feel that the fact that IDE's have found a need for it argues that it should just be in the language. No one would've gone through the trouble of implementing that feature were it not needed/desired by code authors so they could more easily/reliably understand the code.

2 Likes

I would say that the API author/designer should designate that the API requires AND the callers is permitted to use keyword arguments for that API. This prevents the caller from making the argument names part of the API (for semver purposes) if the author of the API didn't desire that. However, it should not be possible for the author of the API to say, "you can use keyword arguments with my API, but, don't have to". IMHO, it should either be, "you can and have to use them" or "you can't use them". Which option is chosen by the API author (through function keyword or attribute). The default, will be as it is today, undecorated functions would not allow nor require the use of argument names by the caller.

On second thought, the "must use argument names", would be a lint that would initially default to warn, and then in the next edition, become deny by default. That way existing API's could migrate to allowing/requiring argument names in a less breaking fashion.

1 Like