[Pre-RFC] named arguments

The bool was an example, the readibility still increase if you can name your arguments for any variable type. Sometimes it's not needed because you're passing a variable with an explicit name but having the possibility to use it is a big win imo.

And builder pattern will never work for plain functions or methods, the majority of my usecases for kwargs in Python, unless you sacrifice UX. So, at least in my head, the builder pattern is way more limited.

I'm sure this point has already been discussed in the 240 comments though so I'll stop there.

What do you mean with the builder pattern not working for plain functions? Do you mean its not applicable to them because of the use of a struct?

Because if so then to me logical approach to that is breaking the API and turning the fn into a builder (which again does not take much effort at all, and is not syntactically or semantically bloated compared to named args).

And while I agree that naming things can make clearer (after all, itā€™s why we do it in the first place rather than using magic values all over the place), we already have something that is better: very strong type checking combined with explicitness of types. The enum example also does this: it explicitly tells you at call site what the name of the enum is, as well as the actual value, assuming of course that you donā€™t import all enum variants into scope (in which case the self-documentation is lost). Unless the type is poorly named that will be way better documentation (not to mention actually verifiable in terms of correct use: there are a few examples posted by Graydon earlier in this thread that demonstrate potential pitfalls) than a regular name will ever be.

At this point, is type ascription an issue anymore? Thereā€™s been no movement on it over the past year, afaict, and there are a lot of issues with it.

1 Like

Is named argument in the roadmap for Rust or is it discarded?

From the feedback I got in this thread I made the following conclusions. A lot of people can agree that named arguments are desirable, as a standalone feature or as a first step towards default arguments. But no ā€œultimate designā€ has emerged and everyone has its own ideas and perspectives about the matter. When an RFC is finally going to be written, I suspect it will get a huge amount of traffic (judging from the size of this thread).

I have not seen any indication from the core team to say it was of the table. On the contrary, some members participated in this discussion and provided some valuable feedback and ideas. But this is not a high priority feature, it is more generally considered as a ā€œnice to haveā€. I donā€™t think any of the core members will take the lead on this, they have a ton of other higher priority stuff to take care of. So if this feature is desired, we (the community) should take the lead on this and push it forward. Until someone takes on the burden of moving this through the RFC process, it will most probably stay on hold for the time being.

My personal opinion about this is that we should probably wait a little. A lot of effort is going into fulfilling the goals of the Roadmap and by trying to push this through the RFC process now, I fear that it might end up being postponed or closed because there are more pressing issues demanding the attention of the core team.

10 Likes

I really like named/default arguments for readability and use them often in languages that have them, like Python.

On the readability side, IntelliJ Rust is adding support for showing the names of arguments at the call site. While not a universal solution by any means, I think this is pretty clever and features like this can help the situation.

Here is an image from one of the related PRs. Note, this is pretty early in the development of this feature and will likely get polished more in the future.

9 Likes

Iā€™m a fan of named args and default values for args and Iā€™m keen to see this land in Rust at some point (together with some other improvements to ā€˜basicā€™ data structures). However, I donā€™t think it will happen this year (even to the RFC stage). I agree with @Azerupi that we should wait a little bit. There is a lot happening in Rust, language-wise, at the moment and I donā€™t think these features will get a lot of support right now (however, in the future Iā€™ll definitely advocate for these things on the lang team and elsewhere).

7 Likes

I absolutely love this idea. It is very useful in Swift. I think they should also add default values for parameters so that you donā€™t have to name out every argument.

3 Likes

Iā€™m interested in this feature and Iā€™ve been thinking how to approach it.

First we should start with motivation - what problems do named arguments resolve?

For me, there are two primary reasons: readability and overloading.

Regarding readability, I find this code difficult to read: connection.send("Hello world!", true), but this code is easy to read: connection.send(message, wait_for_answer). Similarly, I find it easy to read this: connection.send(message: "Hello world!", wait_for_answer: true).

What distinguishes those cases? Itā€™s reasoning footprint as coined by Aaron Turon. In the unreadable case, itā€™s difficult to understand what that true means without looking at the function definition. However, once thereā€™s a name, itā€™s obvious what it does. So allowing naming of the parameters looks like a good idea.

The focus on reasoning footprint can guide us when designing how it should work. Clearly having to type send(message: message) looks boilerpate-y, so it seems to me that itā€™s a good idea to allow omitting the name in case the variable name matches. This is extremely similar to how struct initialization works and in my opinion, it works very well.

I think that making it work just as structs would be also great because of being easy to remember that there are same rules.

Now, letā€™s look at overloading. What problems does it solve and what problems does it introduce? It removes the necessity of inventing a new name when two functions do same thing with different arguments. At the same time, it allows us to shoot us in the foot when combined with some other (mis)features like implicit conversions and makes type inference difficult. (I once spent ridiculous amount of time working with C++ because API of a library I used changed in a way that didnā€™t break my compilation but did break behavior. This situation would be prevented if C++ didnā€™t have implicit type conversion.)

It seems, that overloading isnā€™t a problem in Swift, because of explicit argument names. They resolve both issues: one wouldnā€™t be confused by what the code does thanks to the explicitness of names and at the same time, compiler doesnā€™t have to infer anything, because argument names are basically a part of the function name.

I wouldnā€™t propose that this should include overloading. It should be a separate proposal. But Iā€™d propose to keep the door open, so we can add overloading later.

2 Likes

To be fair to Rust's current state, talks on writing idiomatic Rust by people like @killercup discourage using boolean variables outside FFI code, recommending two-variant enums instead, both for readability and to ensure arguments can't accidentally get swapped when refactoring code that calls said function.

1 Like

I think I agree with everything in your post, but since you didn't say "default" anywhere I want to double check: imo named arguments don't really provide "overloading" unless you're allowed to omit some of them, and we probably should only allow omitting a named argument if it also has a default value. Is that also what you had in mind for the separate future proposal?

I would disagree with that. If you make the parameter names & types part of the mangled/compiled name of the function, then you can has :wink: :

fn foo ( x : i32, y  : i32 );
fn foo ( u: i32, v : 32 );

Which can be 2 different functions called like:

foo( x: 3, y: 4 );
foo( u: 32, v: 64 );

Thatā€™s interesting. Iā€™ve always heard ā€œoverloadingā€ functions used to refer to multiple functions with the same name and different argument types, not different argument names. But now that youā€™ve pointed that out, I think the same motivations apply to both.

So then the question is whether the use cases for ā€œoverloadingā€ are best addressed by default arguments or by actually overloading on argument names, or whether those have enough independent motivation that we want both anyway. That does seem like a question best left to a follow-up RFC.

Probably a better, more realistic example of this would be something like:

fn find_students_by_ssn ( ssn : &str ) -> Vec<Student>;
fn find_students_by_last_first ( last : &str, first : &str ) -> Vec<Student>;
fn find_students_by_last ( last : &str ) -> Vec<Student>;
fn find_students_by_last_city ( last : &str, city : &str ) -> Vec<Student>;

With overloading by parameter name that could become:

fn find_students ( ssn : &str ) -> Vec<Student>;
fn find_students ( last : &str, first : &str ) -> Vec<Student>;
fn find_students ( last : &str ) -> Vec<Student>;
fn find_students ( last : &str, city : &str ) -> Vec<Student>;

Which can all be unambiguously called without the compiler having to do a bunch of magic to determine which is the right one to call.

2 Likes

So, to expound on this point further (apologies if I'm monopolizing the convo), given my example above with searching for students, I could easily do something like:

fn find_students ( ssn : Option<&str>, last : Option<&str>, first : Option<&str>, city : Option<&str>) -> Vec<Student>;

Then, if the compiler could be trained to allow omitting "Option" arguments and auto-replacing them with Option(None) automagically, you could call the above function just like as was shown in the previous example. This however is sub-optimal to my mind for the following reasons:

  • ambiguity is a bigger danger
  • the compiler has to work much harder
  • run-time checks are required in the implementation of the function to handle the optional arguments
  • less clear / readable
  • less hints from the API designer to the API consumer on how to effectively and correctly use the API
1 Like

Yes, that should be a follow-up RFC. I'll just note that I'd prefer actually overloading them, while having a syntax sugar for it. So that if you write e.g. fn foo(pub bar: bool = true), it'd actually create both fn foo() and fn foo(pub bar: bool). The reason this is useful is so one can use either one as a callback. (Otherwise, the one with parameter omitted would be unnameable.)

1 Like

I have an idea that is very uncooked, but i havenā€™t found a discussion about (maybe because its bad :wink: ):

I actually really like the idiom of using descriptives types for parameters.

Imagine being able to do this:

fn draw_cirlce(
    pos: struct Pos{x: f64, y: f64},
    radius: struct Radius(f64),
    color: enum Color { Green, Red} )
{
    ...
}

and on call site

draw_circle(Pos {1.0, 0.0}, Radius(10.0), Color::Green);

So, basically the typed parameter idiom, but with type definitions that are local to the function body and to a scope defined by the parenthesis of the call.

The advantage would be that these types donā€™t have to be imported and they donā€™t clutter up the namespace for the caller.

Any thoughts?

2 Likes

Consider starting small, focused threads on various uncertain parts. It's pretty hard to make progress in a 258-post thread started 2 years ago.

1 Like

Iā€™ll also give my 2 cents from what I have learned from Dart language. The arguments can be both positional and named & it could be backwards compatible.

fn function(a: bool, { b: i32 });
/// Usage
function(false, b: 54);

fn function({ a: bool, b: i32 });
/// Usage
function(a: false, b: 54);

/// Not allowed. Future proof to allow default args.
fn function({ a: bool }, b: i32);

/// Current syntax still works
fn function(a: bool, b: i32);
/// Usage
function(false, 32);

But, it would still conflict with type ascription.

It could also use swift feature of external argument names for an ergonomic api.

fn start({ from start_loc: u32, to end_loc: u32 });
/// Usage
start(from: 4, to: 10);

I am against overloading both fn names and args like swift does, we could use default args & good old trait to achieve the same.

Is anyone working on implementing this yet? Itā€™s a must-have optional feature IMO. :+1: