-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Named arguments #2964
RFC: Named arguments #2964
Conversation
There are far more important topics for the lang team right now. I worry named arguments encourage less well thought out interfaces: If you find yourself wanting this, then you should normally explore more nuanced type usage instead, wrapper types might improve correctness, and chaining builder methods yields more powerful interfaces. At a high level, we should "have fewer joints and keep them well oiled", so some joints we already oil include traits, proc macros, and receiver syntax aka We might discuss whether It clear such proc macros provide enormous DSL capability, either with or without We could ask if docs present builder pattern interfaces well enough ala https://internals.rust-lang.org/t/improve-docs-for-builder-patterns/12126/3 We could discuss patterns and/or proc macros that simplify defining associated structures together, like both a signed and unsigned message, or both a type and some builder, and linking contextual information. We could give
In this, In short, we should not make methods more like |
The RFC explains why these "solutions" are unsatisfactory in the rationale and alternatives section.
First, this works for functions but not for methods. Secondly, this relies on the
I don't think the documentation for builder patterns is the main issue. Creating builder types requires a lot of boilerplate, hence programmers often try to avoid using the builder pattern, and usually apply it only in public functions. I think that private functions should be readable as well! Also note that the builder pattern emulates arguments that are both named and optional, which is a different use case. I also explained this in the RFC.
I'm not sure what you're getting at, could you provide an example?
This has some of the same downsides of the builder pattern, while also introducing a big new language feature that seems controversial and unjustified to me. I'd like to stress that none of your ideas is backwards compatible with existing APIs. The current proposal allows adding named arguments to existing crates, without breaking backwards compatibility. This is a very valuable property, which should not be ignored.
This claim is unsubstantiated. Could you explain what that is better? |
Suppose named arguments are allowed, soon people will ask for arbitrary argument order and optional arguments. The concerns over these features should also apply here. |
No. In this thread, only this RFC should be discussed. If you want to discuss other features like optional arguments, please do so in an RFC that proposes optional arguments. P.S. This is a straw man. Just because "people will ask for x" doesn't mean that we are obliged to implement x. |
We'd benefit far more from stabilizing We'd also benefit far more from reducing the boilerplate involved in the builder pattern, which sounds doable without involving the lang team via improving proc macros tooling. In fact, I suspect the We cannot add arguments to existing methods without breaking backwards compatibility either.
All features carry costs, so their relative costs compared with other features should always be a focus. |
I don't think that these are mutually exclusive. I also don't understand how stabilizing
Right, but we can add argument names backwards compatibly: fn foo(arg: i32);
// can be changed to
fn foo(.arg: i32); Which allows calling |
I like this part. It should be written in huge burning letters. |
@PoignardAzur about this section: being realistic, how often do we need named, but non-optional and non-default parameters in the real world? I might be missing something, but it seems that the only problem the proposed change solves is lack of parameter name hinting in functions with long parameter lists, and this is already solved by IDEs effectively. Is there a plan to go forward with that syntax to also bring default and/or optional named parameters? If named parameters aren't meant to be anything more than described in this RFC, though, I'm not sure the problem they solve even exists. (for anything except blogposts) |
There are multiple issues not addressed in the current RFC.
|
This is exactly the same situation than a badly named function.
The goal isn't to make it easier to write, but easier to read. And nothing prevents library author to make all of their arguments named if at least one is named. |
I am of the opinion that the overlap of functions that would really benefit from named arguments with functions that would really benefit from refactoring is very large. The examples in the RFC are not really convincing IMHO. For example, the In short, I would really like to see real-world examples from a selection of different libraries or applications, rather than theory-crafting with |
It is detailed in the RFC. It's to allow backward compatible migration between positional and named functions. I agree that if backward compatibility wasn't an issue, it could totally be possible to remove this possibility. As explained in the RFC this can be done using a clippy lint.
At repeated over and over, using an IDE should not be a requirement to be able to read Rust code. This is even [detailed](https://github.com/Aloso/rfcs/blob/named-arguments/text/0000-named-arguments.md#parameter-hints-in-ides] in the RFC. Blog posts, git diff, Github, URLO, IRLO, stackoverflow, … don't have IDE capabilities, and it shouldn't be harder to read Rust code using any of those tools than in an IDE.
Please read the corresponding section in the RFC. |
This a very valid question. As an example, nearly every function in the proj crate would benefit from it. This is even emulated in the examples by using local variables (comments are mine):
Rewriting it using named arguments make it unambiguous:
|
Another use case is this module, which calculates sunrise/sunset times depending on the latitude and longitude, using trigonometry. I already refactored it to use a struct for Julian days. But there are still many functions with a less than ideal API, e.g. fn time_of_solar_elevation(century: f64, t_noon: f64, lat: f64, lon: f64, elev: f64) Creating new types (e.g. |
A few more real-world use cases from pub(crate) fn report_progress(
&mut self,
title: &str,
state: Progress,
message: Option<String>,
percentage: Option<f64>,
) {...}
pub fn analysis_bench(
verbosity: Verbosity,
path: &Path,
what: BenchWhat,
memory_usage: bool,
load_output_dirs: bool,
with_proc_macro: bool,
) -> Result<()> {...}
pub fn iterate_method_candidates<T>(
&self,
db: &dyn HirDatabase,
krate: Crate,
traits_in_scope: &FxHashSet<TraitId>,
name: Option<&Name>,
mut callback: impl FnMut(&Ty, Function) -> Option<T>,
) -> Option<T> {...}
pub fn iterate_path_candidates<T>(
&self,
db: &dyn HirDatabase,
krate: Crate,
traits_in_scope: &FxHashSet<TraitId>,
name: Option<&Name>,
mut callback: impl FnMut(&Ty, AssocItem) -> Option<T>,
) -> Option<T> {...}
pub fn find_node_at_offset_with_descend<N: AstNode>(
&self,
node: &SyntaxNode,
offset: TextSize,
) -> Option<N> {...}
fn adjust(
db: &dyn HirDatabase,
scopes: &ExprScopes,
source_map: &BodySourceMap,
expr_range: TextRange,
file_id: HirFileId,
offset: TextSize,
) -> Option<ScopeId> {...}
pub fn add_crate_root(
&mut self,
file_id: FileId,
edition: Edition,
display_name: Option<String>,
cfg_options: CfgOptions,
env: Env,
proc_macro: Vec<(SmolStr, Arc<dyn ra_tt::TokenExpander>)>,
) -> CrateId {...}
pub fn add_dep(
&mut self,
from: CrateId,
name: CrateName,
to: CrateId,
) -> Result<(), CyclicDependenciesError> {...}
fn dfs_find(&self, target: CrateId, from: CrateId, visited: &mut FxHashSet<CrateId>) -> bool {...} These are just the most obvious functions that would benefit from named arguments, which I found by skimming through parts of the workspace. |
As proposed, this wouldn't allow argument labels to be added to trait methods backwards compatibly. The requirement that labels in trait implementations must match the labels in the trait definition means that adding a label in the trait definition invalidates existing implementations. I think, in order to be consistent with the motivation for this design, labels in implementations should be optional, but if present, must match the corresponding label in the trait definition, and must follow the rule that positional parameters cannot follow named parameters. The labels defined in the trait definition can be used at the call site irrespective of whether they appear in the implementation for a particular type. |
@stevenblenkinsop I think this is a good idea. Given this example: trait Trait {
fn foo(&self, .arg: i32); // named arg
}
struct Struct;
impl Trait for Struct {
fn foo(&self, arg: i32) {} // positional arg
} Then the following function calls are equivalent: Struct.foo(42);
Trait::foo(&Struct, 42);
<Struct as Trait>::foo(&Struct, 42); So it should be allowed to use named arguments in all cases: Struct.foo(.arg = 42);
Trait::foo(&Struct, .arg = 42);
<Struct as Trait>::foo(&Struct, .arg = 42); My only concern is, does it make sense that (After all, the argument patterns can differ between the trait definition and the implementation, too). |
Since this is only syntactic sugar, you could argue for However, the main reason for forbidding the named use of positional arguments is so that naming an argument (adding semver guarantees) is an opt-in choice; in this case, the API is owned by the trait definition, not by its implementation for the struct, and the trait owner already opted-in to named arguments - so there's no reason to avoid them. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you provide feedback on my comments?
text/0000-named-arguments.md
Outdated
|
||
foo(.a = 1, .b = 2, .c = 3); // ERROR! 1st argument is not named | ||
foo( 1, .b = 2, .c = 3); // ok | ||
foo( 1, 2, .c = 3); // ok |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer all or nothing, so no mixed cases, if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this flexibility at the call site be motivated? The motivation for names being optional at the call site is so that names can be added to existing APIs. This would be served by an all or nothing requirement at the call site, however. Is the idea that an API might initially not define a name for the first argument, but later decide that it wants one? Or that APIs might define a name for the first argument where one isn't needed, so the call sites should have the option to leave it off? I'm not sure allowing these cases warrants having quite so many different ways to call the same function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise we have a situation, where making a positional argument named in a function that has no named arguments is not a breaking change, because nobody could be calling it with named arguments yet; but making an argument named in a function that already has some names is breaking, because you might have users calling using those names, and those calls will become calls with partial use names. It is true that the first case is important so that introducing this feature doesn't break APIs, and the second one isn't; but still this seems like a strange inconsistency.
IMHO, removing call-site flexibility is only tenable if the function definition is required to have all-or-none (in which case, once you add names, you can't add any more names).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, the reason was that I wanted to allow specifying only some of the argument names. However, I think that positional args after named arguments should be forbidden, to be forwards compatible with optional args and named args in arbitrary order.
Currently, this means that named arguments can be added backwards compatible from right to left, although that is not the primary motivation:
fn foo(a: i32, b: i32, c: i32);
fn foo(a: i32, b: i32, .c: i32);
fn foo(a: i32, .b: i32, .c: i32);
fn foo(.a: i32, .b: i32, .c: i32);
If optional args are added in a future RFC, this will be much more flexible. So when omitting an optional argument, arguments after the omitted one must be named, but arguments before the omitted one can be positional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I commented that we need take into consideration the concerns over optional arguments and arbitrary order you said that was a straw man.
Then what's this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to make sure that optional arguments will be compatible with this RFC, if they are added in the future. However, there is no guarantee that they will be added even if this RFC is accepted. That's why I said that we should concentrate on the features proposed in this RFC, and not start a discussion about benefits and downsides of optional arguments, since that can derail the discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the strawman was referring to this framing:
Suppose named arguments are allowed, soon people will ask for
which is actually Slippery Slope (but still not a relevant argument). Forward compatibility with those features should be guaranteed, but the merit of this RFC should be judged on its own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Linking the intended behavior by the lint should be fine here.
@stevenblenkinsop I added your idea to make argument names optional not only in function calls, but also in trait implementations. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reason for not reordering variables (Rust does currently not support this? and the authors opinion) is missing.
The planned lint behavior (default behavior/changeable behavior?) should be worded/explained more specific.
However, it should be allowed to omit the argument name, when it matches the variable, field or call expression that is passed as the argument, for example: | ||
|
||
```rust | ||
fn foo(.arg1: i32, .arg2: i32, .arg3: i32) {} | ||
|
||
let arg1 = 42; | ||
let s = Struct { arg2: 42 }; | ||
fn arg3() -> i32 { 42 } | ||
|
||
foo(.arg1 = arg1, .arg2 = s.arg2, .arg3 = arg3()) // no warning | ||
foo(arg1, s.arg2, arg3()) // no warning | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should belong to the function section. I am not sure, if it is very smart to add work to the compiler to check this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a lint, so it doesn't add work for the compiler, only to cargo clippy
.
Hey, thanks for doing this RFC! Not sure about the dots that are already used a lot but well, they are easy to write (I have a preference for I'm actually so glad there's a RFC for this and that it focuses on just that and not optional arguments that are indeed usually closely linked to named arguments but allow to too many digressions that corrupt the discussion. |
Which brings up a great point -- if named parameters are so terrible why are they good for struct constructors? What argument against name parameters doesn't also apply to struct constructors? The syntax for struct constructors I think is one of the things that makes the language more clear, I just think you should be able to use that syntax for more functions. The point that you are forced to specify all of them is only true so long as we also don't have default arguments. That's a separate issue but comparing against struct constructors is again informative -- the lack of ability to specify defaults for struct fields leads people to resort to |
@wiogit Please read the RFC. Why make named arguments opt-in? |
For me it comes down to convention over configuration. A struct is purely comprised of named fields or purely comprised of positional fields for tuple structs. The name/position design is chosen for the entire struct, not for each individual field. Yes, there is added clarity by having structs constructors to use named fields, but it is also the only way it could be done while keeping the ordering of fields as an implementation detail. What is your opinion of the following code? struct MyStruct(String, .x: f32, .y: f32);
let my_struct = MyStruct("East City".to_owned(), .x = 25., y= 0.);
let my_tuple: (String, .x: f32, .y: f32) = ("North City".to_owned(), .x = -40., .y = 0.);
enum MyEnum(u32, .Decimal: f32, .Undefined);
let my_enum1 = MyEnum::0(40);
let my_enum2 = MyEnum::Decimal(40.);
let my_enum3 = MyEnum::Undefined; |
@wiogit - Except for the dots in front of the variable names, which are awful (unnecessary, and bring to mind unrelated things), the struct looks fine. There's no point having partially-named tuples and structs, so why bother with the second? And the dots are still ugly / distracting / pointless. The enum is awful--the whole point of enums is to be able to talk about the alternatives, which means they need names! Anyway, I don't see how this is related to functions getting external argument names. Functions already have argument names. The thing is that the names aren't part of the public API, and the question is whether that should be possible. It's a different issue than whether we should be able to elide argument names entirely. This does point out a weird aspect of function argument lists. From the inside, they look identical to regular structs: struct Coord{ x: f64, y: f64 }
let c = Coord{ x: 2, y: -2.2 };
// can talk about c.x and c.y
fn foo(x: f64, y: f64) {
// Can talk about x and y; 'c.' is implicit
} But from the outside they look identical to tuples!
This is...kind of weird. But we all accept it without blinking an eye. Furthermore, why the asymmetry between arguments and return values? Mathematically, a function is just a map between two sets. fn swap(x: f64, y: f64) -> (f64, f64) { (y, x) } The return value is unabashedly a tuple, which is the canonical un-named form of tuple structs, which is a struct. You know this because you can let v = swap(1, -1); You're not required to take apart the pieces (though you can if you want to). So, what's the deal? Function outputs are exactly tuples, i.e. structs. If this is cool with outputs, why isn't it cool with inputs? Why isn't it cool to at least adapt it to look that way, even if the implementation may differ; or to allow functions for which it is true even if it isn't true normally. My point is that there are a lot of sensible ways to think about things, and if Rust shifts a bit from one way to another, as long as it doesn't end up incoherent, I don't think we should oppose it reflexively as being "not Rust". Some things aren't Rust, and we should identify those and keep them out. But things that are consistent with what we've already got should be considered as potential changes/additions. (Obviously, subtractions are a problem for backward compatibility...and if you can only add and not subtract you should have a high bar for acceptance since you can't fix your mistake.) So this is why I think the let's-tack-on-an-ad-hoc-syntax-for-functions-that-we-already-solved-in-a-different-way-for-other-parts-of-the-langauage approach is a bad one. It's pure complexity added to the language, because it doesn't accomplish its goal by reimagining what is sensible and making a new sensible whole. It's just new rules, coming out of nowhere, that you couldn't possibly have guessed from learning about other parts of the language. (And notationally, tacking on symbols to things makes code feel less elegant, which is another downside. But even if it was |
@Ichoran do you have confidence that Rust is open to reevaluate the meaning of all its data structures to make them mutually compatible, instead of having an incremental addition? For me this would be a miracle in language evolution, I've never seen a language refactor its underpinnings after the ship has sailed for too long. |
I think a much simpler alternative would be to allow anonymous struct declarations in parameter positions. This would be conceptually like a struct-name version of lifetime elision. struct FooArgs {
x: i32,
y: &'static str,
}
fn foo(f: FooArgs) {}
foo({ x: 1, y: "hello"}); |
@oblitum - The incremental addition would be to add a little syntactic sugar to make it easier to utilize a similarity that's already there. I don't know why you feel that this is a dramatic, daring step in language design. It's no more dramatic conceptually than field init shorthand. |
One quick note: this doesn't require full structural records. We've talked about having type inference, such that |
Moderation note: As some of you may know, RFC discussions can be particularly straining on folks, especially when there are a lot of comments in a short period of time. There is even some discussion ongoing on how best to modify the process to make it easier on everyone involved. Until that time though, I'd like to remind everyone to be their best selves so that we can move forward collaboratively. In light of that, I'd like to urge everyone to think about these things when responding:
Thank you all for listening! |
I think any feature added should be just syntactical sugar for a more verbose syntax. This is hence not preventing a later .a = 1 syntax, but rather outlining a road to it: Given I have a:
The first thing is I want to be able to create a struct, so that I can do without macro usage:
The problem one is that right now it’s not possible to even create the arguments of a function. This already gives a lot of trouble for generator parameters or creating a generic wrapper for async fns or closures. Any fn defined MUST BE where F: Fn(int32, int32) It’s not possible to define a generic callback fn type - the compiler can however derive the type automatically. However as has been expressed already, the argument to a function is essentially a named tuple. Here we go back into the isomorphism territory and I agree that conceptually:
Is not the exact same semantically as:
However storage and internal representation wise they are memory layout very similar IIRC. So we can say that functions essentially right now take an anonymous tuple struct. So a fn add(i32, i32) is in reality of type:
So every function in Rust essentially takes a struct tuple as Parameter. Now once we are this far that we can call a fn differently using a tuple, it is clear that we can convert from a struct into this named tuple. And now that we are here we can also just use [#derive(NamedArguments)] to make it all work as now we should be able to define an Autotrait NamedArguments that allows to use a named struct in addition to what is essentially a tuple struct. And then once we have that, we can discuss again if we want to automatically desugar:
to ({a: 1, b: 2}) but syntactic sugar wise this is the last step, not the first one. There must also be a way to programmatically create arguments tuples and argument structs for this syntactic sugar to work even however. So my vote would be to postpone on implementing tuples, from conversions, etc. and just then discuss if we want to have .a = 1 Notation or not. That also decreases workload on rust core team. |
Many comments here discuss the syntax at the call-site. While this is also an important consideration, the main hurdle for named arguments, which is addressed by the RFC and not addressed by (edit: some of) these comments, is the syntax at the definition site: how to mark named arguments as opt-in, possibly granularly, so that neither implementing this RFC nor changing the name of an argument aren't breaking changes. Such syntax cannot be struct-like, as the only parallel from structs is marking a field as pub, but this does quite other things (in addition to making the field's name part of the public API): in particular, a struct constructor cannot be publicly called unless all of its fields are pub. Using this syntax here would be quite confusing; function arguments are always "public" in this sense, just their names (sometimes) aren't. (On an aside, to me this demonstrates that struct constructors and function calls are not that similar; the reason here is that the first is usually a private implementation detail, while the second often public API). So one question that arises (and was discussed only briefly) is whether partial opt-in (mix of named and positional arguments) is necessary or not. If it isn't, maybe it could still be possible to model this as a switch selecting whether a function's arguments are a tuple or a struct. It seems that at least for reciever syntax a mixed mode is required, this could be perhaps special-cased to have Another concern is how to match the syntax at the definition-site and the call-site. To me it seems much more important that these two match up than the call-site syntax matching struct constructor. So if some novel syntax needs to be added to the function definition, it might as well be used in the call-site as well. |
First of all: I am am proponent of named arguments and like your syntax proposal. I am fairly new to rust, and that was like my first feature request ;) that I googled. I am trying to find a way forward for rust itself, so that it's easy to implement the syntax sugar on top of something complex manual, but working.
That is indeed true, a macro would automatically make all parameters named, which is obviously not wanted.
Sorry - I realized, that mixing is not possible, I think it should be though ... So consider that a hypothetical example.
and then access them via I think that this would match pretty close to positional parameters + named parameters, further showing that there is some similarities between parameters and structs.
The problem is that function arguments in rust are not a first-class citizen right now, which makes that rather difficult to do this at the moment. I was not able to find the history why the magic
Agree. |
We discussed this in the @rust-lang/lang triage meeting today. Here's the consensus from that meeting: We're not entirely opposed to the concept of better argument handling, and we've followed several of the proposals regarding things like structural types, anonymous types, better ways to do builders, and various other approaches. However, we feel that this absolutely needs to be discussed in terms of the problem space, not by starting with a specific solution proposal that solves one preliminary part of the problem. And while we don't want to let the perfect be the enemy of the good, we do think that taking an incremental step requires knowing what the roadmap looks like towards the eventual full solution. Between that and our roadmap this year being more about finishing in-progress things rather than taking on major new features, we're going to close this RFC. When we're ready to work on this, we'd want to see a proposal in the form of an MCP; however, we're not looking for that MCP this year. |
@rfcbot close |
Team member @joshtriplett has proposed to close this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
The final comment period, with a disposition to close, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. The RFC is now closed. |
This RFC adds named arguments to functions. Named arguments must be prefixed with a dot, e.g.
I want to thank everyone who participated in the internals discussion!
🖼 Rendered
goto summary, motivation, guide-level explanation, reference-level explanation, drawbacks, rationale and alternatives, prior art, unresolved questions, future possibilities