Oh no, please absolutely don't. It's great that we finally got rid of ad-hoc overloading in a mainstream, useful language. It's got no place in a system centered around safety and correctness.
I'd like to point out that purely name-based overloading is completely superlfuous because you can just write a different function with a different name. Foo::new(bar: 42) and Foo::new(qux: 43) are not in any way better than Foo::new_bar(42) and Foo::new_qux(43); they are only noisier and create yet another bifurcation point in API design that nobody ever will agree on.
But identifier is an expression too, and there's no way to tell apart expr: expr and expr: type at parse-time. It looks like you are already thinking about typecheck/semanal time, but how does this get represented in an AST? Tying the parser and the typechecker together is not an option.
There's a lot of long-winded self-advertising under the "rationale" section, but you only mention Swift in a single paragraph, and you never address the main pain point there. Swift is a language where named arguments existed very early on, and they have been a massive pain in the neck from the get-go. In particular, they don't interact well with type signatures where one must describe function types with named arguments. This problem is most visible in the case of higher-order functions, which are prevalent in Swift and Rust alike. It is still not clear to this day what the correct solution is to typing such functions:
should named-argument functions have the same type as their anonymous counterparts? This competely cancels out any advantage of named arguments, because the indirect call will not (be able to) use named arguments.
should there be different signatures, ie. should names be part of the function type? This has the enormous disadvantage that it won't be possible to pass a function with non-public argument names and vice versa, which will create more boilerplate (unnecessary wrapper closures) than needed.
Nobody ever seems to address, let alone solve, this problem. Swift itself has switched sides several times. No named arguments RFC can be taken seriously without considering and proposing a solution.
So by your own logic, this argument-name-based "overloading" is just another function, and it's got no place in a correctness focused language. I apologize, but it really seems like you've contradicted yourself in two adjacent points.
This RFC does not ask for type-based overloading, and I agree that Rust has no need for type-based overloading. However, if Rust were to have argument labels, it only makes sense that argument labels would be part of the function name, and thus "overloading" based on argument labels would be allowed — because it's not overloading at all.
This doesn't quite match my experience. Higher-order functions are an interesting wrinkle in design, but the Swift API's use of argument labels is generally seen positively. And we can learn from Swift here as well.
The standard solution is that labels don't exist in the type system, and are merely sugar for naming the mangled function name. I don't see how that is a failure to solve the problem.
(I still would like to not indicate being for or against this feature and this RFC in particular.)
Hi, I’m the author of the Swift Regrets articles mentioned above, and one of the early Swift language developers (though no longer). For me, the two articles that got called out above were about the implementation of named arguments more than the feature; I think most of us still think the feature is a good one. That said, the point made above about named arguments being designed into the language and stdlib from the beginning is really important (and it was massively disruptive when we changed the guidelines to what they are today, a few years in).
I miss named arguments in Rust, not just for new_foo and new_bar but for any argument where the type information is not strong enough to clarify the purpose, often &str, Duration, or None. You can newtype these, sure, but if the only reason to newtype is clarity at the call site? Then it’s not pulling its weight.
But this “I miss…” is a snap-my-fingers kind of wish. Adding named arguments to present-day Rust is something that should be approached with caution, and the rules won’t come out exactly like Swift’s (which for better or worse has type-based overloading as well). The migration path for both the stdlib and existing crates out there is a difficult problem. So I appreciate both the thoroughness of the proposal and the thoughtful critiques in the thread.
I do recommend people check out the Swift API Design Guidelines to see what argument names look like in practice in a language that’s had them from the start.
On overloading: I agree that the correct user model for labeled arguments is as separate “full names”, not overloading on the “base name”, even though the compiler will have to handle them kind of like overloads for error recovery and partially-written code. The fact that Swift lets you refer to a function by its base name only is not conducive to library evolution, and I wish we hadn’t allowed that. (We never managed to come up with a “full name” syntax for a zero-argument function, though.) I think this means that if labeled arguments are added to Rust, the base name would only refer to the function with no labels, and any full names with labels would have to always be referred to with the labels included. (This implies no overloading on arity without also having labels.)
A lot of the troubles we had were in preserving labels in function types. Don’t go down that route.
On optionality: Swift differs from Python and C# in that the argument labels are required at the call site if present. Why? There are some technical reasons, but mostly it came down to wanting the same call to look the same everywhere, giving library authors control of how their API is used. It also makes evolution much more straightforward; you don’t need to worry about collisions with other functions in your library. And because people get types wrong much more than they get labels wrong, the labels are important for diagnostics when multiple functions share a base name. Separating the label from the local parameter name also allows them to be different, which is important because good labels are not always nouns.
On reordering: This goes a lot with the previous one, but mostly only at the abstract level of “the library author should design their API with intention”. I agree that many times it’s not important, and that it’s somewhat inconsistent with how Rust does structs, and I can’t put my finger on a more concrete reason to disallow reordering besides “it’s simpler for the compiler”.
On name: name: this is a Rust feature that I miss in Swift, so if you can figure out how to make a plain name work here I would love it. One way to do so is to disallow unlabeled arguments after labeled arguments, and disallow reordering; that way, you know that if you have another unlabeled argument, it’s actually labeled with the same name.
FWIW, I'm also currently uncertain about named arguments. They seem like they're only really for this weird middle ground, since if something takes 1 argument it doesn't need to be named -- I'm totally fine with new and with_capacity instead of overloading new() and new(capacity: -- and if something takes 4 named parameters I bet it'll want 5 named parameters soon, and thus it should be passing a real type instead.
So yes, named parameters are good for functions which are used more than once and have, say, 2-4 parameters and have multiple parameters of the same type and it's not something (like [T]::swap) where the order of them is irrelevant.
Thus the insert example isn't great to me. Its type is &mut Vec<T>, usize, T, so the at is just noise in every case except T == usize. I don't need named arguments for .insert(0.0, 1), since it doesn't compile. And if I'm reading code, it's usually more like .insert(i, x), so it's not surprising which is which. (Basically, this is me agreeing with yigal that named arguments are super-important in Python, but much less so in Rust. Note that, thanks to us not having integer coercion, even .insert(0_u8, 0) is an error, unlike in many other languages where a u8 can coerce to usize.)
The example of "oh, but you could use with as the argument name for a builder" is incredibly unpersuasive to me. You could name every single parameter ever "with". We have types in rust, and the fact that you're passing a ConnectionOptions is the important thing here. Forcing people to type with: in front of it is entirely noise.
My strongest feeling is that I want to see ekuber's default field values RFC first, to explore a kind of "builder pattern lite" to see how that impacts things before going all the way to something as pervasive as named arguments. (Notably, that RFC won't propose changing name resolution, trait matching, etc the way this one does.)
That RFC would, for example, address the "and also can't handle non-optional arguments" objection in #12 above.
to this. Struct initializers using : is my biggest complaint with Rust syntax -- It would be so much nicer if = was for values and : was for types, rather than the confusing mix we have today.
I'm not sure this is a meaningful distinction, since I could just say "it's not overloading, it's using the parameter types as part of the function name". And indeed, that's how overloading is actually implemented in things like C++.
But more importantly, if I can import two different functions with use yourcrate::foo;, then I'd say they're "overloaded", regardless of the exact details. Because that means their name is just foo, not foo(a:) and foo(b:).
There might be something to tease apart here.
If you're looking to "iterate quickly", then hopefully that's not breaking changes to the crate's public API. This is my biggest gripe with C#'s named parameters, actually, that all parameter names (on exposed things) are always a semver guarantee. And thus I'm glad to see the proposal here doesn't instantly make ever parameter name part of semver.
But for things that aren't part of the public API, I'm far more sympathetic to "ok, this function is kinda terrible, but whatever, it's just a module-private helper I call three times inside this file to reuse some logic".
That opens up some potential freedom, the same way as non_exhaustive does. We could, for example, provide a slightly-ugly syntax in the caller that would use the existing names, but that's only allowed for calling things defined in the same crate. So if you have a crate-local helper, sure you can call self.Handle(request, @@loadCookies = true, @@followRedirects = false); if you want. (Placeholder syntax, of course.) But you probably shouldn't make a function like that part of the public API. It's basically the same kind of idea as "sure, maybe you use a 4-tuple inside a function to store some stuff, but think carefully before you put a 4-tuple in a public API".
That would remove a bunch of the complications here, like signature changes, overloading, updating Fn traits, etc. But I can see some extra compilations there too, like maybe "crate" is the wrong pivot here -- something from a derive might be "in" your crate but that doesn't necessarily mean you should be able to use its parameter names, so maybe it's more like "same hygiene context" or something.
Hmm, that makes me wonder about hygiene for argument names. Can a macro make an argument name that can be used only from inside the macro, but not outside of it?
I did not. If this proposes full-fledged overloading, then it's "useful", but very dangerous. If instead it just proposes a pure syntax sugar, then it's not dangerous, but useless.
It's hardly a "standard". I already explained why that is a problem.
Thanks everyone for the interest, that was way more than I expected ! I'm not a native English speaker so don't hesitate to point out spelling mistakes, they can easily slip by.
You all gave me way too much feedback to go through in an evening but I'll try to take it all into account and will post a message here when the pre-RFC text has been updated in response.
Objective-C is a shitty reimplementation of Smalltalk. IIRC, Apple bought nextstep which was implemented in Obj-C by people who wanted to use Smalltalk and its design philosophy and idioms in an OS. That precluded the use of the smalltalk binary image design, so they created this lower-level hybrid with smalltalk semantics and a C like compilation model. So the platform was designed with named parameters from the get go in its APIs.
Smalltalk has a very elegant design for a dynamic language and as part of that design instead of function names, it uses the parameter names as the "message name" (read: function name). And I agree, that it is very elegant and allows for some neat API designs. If you squint at Obj-C, you'd see some of this shine through due to its history. It is just a different approach with a different set of trade-offs with regards to API design.
Having said that, Obj-C itself has problems, as you noted yourself, and isn't the best candidate to draw inspiration from. I'm sure that to a large extent, the reason swift has this is due to its role as a replacement for Obj-C and the need to be compatible to the aforementioned platform APIs.
Rust however does have function names (it comes from a very functional background in ocaml) and mixing these two alternative solutions to the same problem just adds redundant complexity to an already large and complex language. It's just a redundant decision point in API design that would bring endless debates and have cognitive costs for everybody using Rust.
If anything, this comment is quite rude on your part. This topic has come up numerous times in the past and there is good reason why it was rejected multiple times. This RFC changes nothing in that regard. The expectation that people ought to invest their time to re-explain the same thing over and over and over is rude given that the onus is with those wanting to change the status quo.
The example above changes nothing in the calculus here. The idiomatic ways for API design in Rust to solve this are to use a plain old struct literal that groups all the parameters together or use a builder pattern when a more complicated construction logic is needed. Having as many parameters in the above example feels smelly to me regardless of their names' status.
For me, the most readable solution would be to introduce a Filters struct around these parameters (all together, and not individual newtypes as in the strawman argument above).
Moreover, I'd say that this actually calls for another feature entirely, inferred struct literals. i.e
let num: i32 = 12; // inferred already to: 12_i32
let f: Foo = _ { a: 12 } ; // this literal could be likewise inferred to: Foo { a: 12 }
This could than be used to make the example look like the following snippet to satisfy the OP:
compile_opts.filter = ops::CompileFilter::new(_{ // inferred from the function signature
library: LibRule::Default,
binaries: FilterRule::All,
tests: FilterRule::All,
examples: FilterRule::none(),
benches: FilterRule::none(),
});
This actually fills a gap in the current design instead of introducing an alternative design. It is an intuitive and composable solution. We already have inferred numeric literals and this is a natural extension of that, so it matches existing expectations and keeps the "there's one way to do it" best practice.
What I'd encourage you to do is concentrate on the "what problem is this solving?" part. It's awesome that you've written up all the details for how this would specifically work, but mostly you're going to get motivation-level feedback right now.
For example, I commented about :-or-= above, but I'd actually suggest you ignore that part for now, because it's not that important, really. It could meet its goals with either syntax, it's front-end so it could be changed relatively easily, and it doesn't impact harder questions like how it works with Fn or use or what have you.
That will hopefully help you avoid too much redrafting, and maybe focus the discussion more. For example, it might be easier to discuss the goals rather than overloading-vs-defaults as mechanisms for doing different things.
Bonus points if you can phrase the "here's a problem worth solving" part entirely without ever saying "named arguments". I likely agree with "gee, I wish there was a better way to _______" even if I'm not currently convinced by named arguments as the specific solution.
I disagree on this point. Passing an argument to a function is already a kind of assignment. The variables in the function call are just copied/moved to the function arguments exactly the same way as an assignation with the equal sign.
In the past, when I've found myself desiring named arguments in Rust I've ended up using a macro to emulate them. Laying out how this can be done, and the limitations of this approach seems relevant to this discussion.
If all calls to CompileFilter::new go through the macro, then the argument order only needs to be gotten correct once.
Limitations
Using the correct order is required, unless one is willing to write (or write code to generate) n! match arms. And even then, that may cause compile time to increase significantly.
Optional arguments are possible, but the only (relatively) simple methods I'm aware of for them involves a macro internal variable per field and/or an arguments struct.
The compile errors for small mistakes in the format are often not very informative. "no rules expected this token in macro call"
All generic reasons one may have to not want to use macros: Harder to implement good IDE support, extra friction in publicly exposing them, etc.
I suppose, given one wants to encourage people to be considerate when exposing named arguments, the friction associated with exposing macros to other crates could be considered a feature.
I don't see how adding friction for the users of the API is desirable.
That is actually what I had in mind when I wrote that comment. However, I believe ekuber's RFC was never submitted (or finished?), which is unfortunate, because it's such a useful feature.
A better question is why would you allow that. The make of the function put the parameters in a specific order and with specific names so that the usage site reads naturally. Changing the order will make the reading wrong.
Function calls sometimes have one (or more) very long argument, and in that case it (IMO) usually improves readability to put the long one last. As an API designer, I try to order my functions' parameters to gain that benefit, but sometimes the best order by that metric varies by call site.
Sounds like a code smell. If the argument is too long it ought to be extracted to a variable with a meaningful name (which I sincerely doubt would be exceptionally long)
We can have poor man's structural records tomorrow. As a library. I brought up this technique in URLO, thread Named arguments patchwork, which was not entirely serious.
You can misspell struct field names just as easily, and then you can't fix the interface without violating backwards-compatibility either.
You argue that using meaningful types is "correct design", yet standard library clearly doesn't use these consistently (using usize for indices being highly non-semantic, even limiting in that a special type could handle indexing from the end). I therefore feel like the main motivation isn't "correct design", but inertia.
You argue that languages where named params make sense are all dynamically typed, clearly not considering Swift, C#, or Scala.
It's you being rude, not the OP. That you feel insulted by this post being duplicate of older ones - and complaining about it instead of eg. providing the links to the older posts - is something I don't get.
For information, some other posts about named arguments:
Moderator note: Please refrain from personal attacks. You can take issue with someone's behavior without making unkind remarks about their personality.
I really would just like to make a bit of a meta remark at just how refreshing it is to see such a well-written RFC written by somebody who has clearly read all of the past discussion, and who has new ideas to bring to the table on a topic that has been mostly running around in circles for years.
The proposal here is extremely cohesive and has a well-defined scope. I can see how each design decision and limitation (e.g. lack of reordering, forced usage at callsites) makes sense in the context of the rest of the design. At first I was unsure why overloading is a part of it, but as I read through it became clear that this was necessary to allow backwards-compatible adoption by existing APIs. You've certainly found a local maximum in the design space, and I don't think any small part of it can be easily tweaked without changing a bunch of the rest.
There are not any concrete criticisms I have been able to come up with which are not already at least acknowledged by the proposal. (For instance, the fact that callers may be forced to write name: name.)
So, about the proposal itself? Well, considering the fairly limited scope of the problems it is designed to solve (especially in comparison to other named argument proposals):
Providing library authors with an easier (for the author and for the user) alternative to newtypes/builders for a function which faces the problem of, "it can be unclear what this bool parameter is at callsites."
Letting a library author provide ::new(bar: ...) and ::new(foo: ...) rather than ::new_bar and ::new_foo
I feel the benefits are unlikely to outweigh the costs in additions to the language:
Addition of parameter names to the type system (in particular to fn types and Fn traits).
Argument names playing a role in function name resolution.
Syntax for:
defining named parameters
using named arguments
qualifying overloaded functions
and thus, I generally wouldn't expect to see it accepted in its current state.
The idea of "named arguments" brings with it a lot of baggage and expectations from other languages, and I get the feeling that many users would be excited to hear "rust is getting named arguments" only to then be frustrated and confused by the restrictions here. "Overloading via named parameters" is perhaps a more direct description of the idea.