-
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
Wishlist: functions with keyword args, optional args, and/or variable-arity argument (varargs) lists #323
Comments
depends on how you see it. the API will definitely be well-considered given the constraints of no default/kw arguments. but once those are added, i’m sure the API will be considered lacking, especially in areas where new defaults render old functions obsolete. a good example (if the new syntax sugar wouldn’t exist) would be fn slice(&self, start: uint, end: uint) -> &'a [T];
fn slice_from(&self, start: uint) -> &'a [T];
fn slice_to(&self, end: uint) -> &'a [T];
|
@flying-sheep so then you deprecate such methods, just like today, no? |
@pnkfelix I think his argument is that we don't want to be stuck with a ton of deprecated cruft in the stdlib, but I'm still personally not too sympathetic to the need to have default arguments before 1.0. |
yeah, that’s my argument. either cruft, or lengthy deprecation formalities and rust 2.0 a year after 1.0 (semver requires a major version bump for breaking changes) |
While I'd prefer to have optional/keyword args before 1.0, I believe the problem with deprecated functions crufting up the API can be substantially lessened by removing such functions from the generated API docs. This is how the Qt project (and D AFAIK) handles API deprecation; the deprecated stuff continues working but developers writing new code don't see it. Of course, the generated docs should have a setting/link/button to show the deprecated API items but it should be off by default. I think this is also a good idea in general; just a couple of days ago I accidentally used a deprecated function because it seemed like a good pick and I didn't notice the stability color. |
Rustdoc's handling of deprecated items definitely needs some improvement - see rust-lang/rust#15468 for some discussion. |
See the "Struct sugar" RFC for another take. |
I'd like to see some of these RFCs revived in the near future, if someone has time to do so. |
Agreed, there's a whole bunch of different keyword arguments proposals floating around and there's been a few discussions which seemed to die off a few months ago... would love to hear the current standpoint on this. |
Ok, 1.0 released, even more, can we please discuss it again? especially default arguments. |
This issue is open, it's free to discuss. |
(though its possible an https://internals.rust-lang.org post might be a better UI for undirected discussion ... we didn't have the discuss forums when we set up these postponed issues...) |
I'd love to see keyword arguments. I opened a thread on /r/rust with some comments about them before finding this issue. I guess /r/rust is an appropriate place for "undirected discussion" too? |
In any case, this should be done in such a manner that it does not cause very inconsistent libraries, perhaps by letting the named parameters be optional? For example the names could be given by the argument name. Such that, the function: fn func(a: u8, b: u8) -> u8; can be called both with and without named parameters, for example: func(a: 2, b: 3) or something along this, while still being able to do: func(2, 3) |
Also, this feature could easily be misused by taking named parameters instead of structs, which, I think, is a bad thing. |
I think it's because supposedly people are thinking about some kind of heterogenous variadicity (like the case of println, which is currently done with macros), and that isn't possible with arrays. |
I see, but that's why we got macros. If you want heterogenous variadicity, you gotta go with macros, after all the Rust macro system is very powerful. |
I agree, macros are appropriate for this. |
Bump, this seems like a basic critical necessary feature for developer sanity, to be able to use keyword arguments dramatically promotes readability and also enables us to reorganize our code better. If we have to make a struct for every function just to pass keyword arguments, without wanting the struct, i'd argue that's boilerplate if rust is truly a safe language then positional arguments in general ought to be deprecated, then there is exactly no way to screw up ordering of arguments and you don't need to make a struct just to make a function. |
Half agree with this. The "safety" of rust is primarily memory, although if a language feature can decrease errors it indeed ought to be so. That said, there still are clear cases where positional arguments are superior in my opinion, like when there is only a single argument; or when the order of the arguments doesn't matter. Instead I think this should be done on a per function basis. So for some functions you must use keyword arguments. For some you can use positional arguments. |
The syntax could be that if keywords (named arguments) are distinct, the keywords could be left out from the statement? This makes the syntax concise but still safe against errors. Maybe have a look at Kotlin for inspiration, which even allows mixing positional and named arguments to some extent. |
There were a lot of discusion about it and Kotlin (which I initially also proposed) is actually a bad example. Using Kotlin approach will mean that changing any argument name is a breaking change regarding semver. Dart is in fact a much better example - both positional arguments and named are explicit and selected by the function author. That way there is a guaranty that changing name of the positional argument is not a breaking change while it is for the named one. It will also be much easier to add without breaking anything. fn do_something(pos_arg: usize, { named_arg: bool }) {
}
do_something(0, named_arg: 10); On the other hand I would agree that the way Kotlin does default values is arguably superior to everything else I've seen so far. And default arguments probably shoud be the next step after the named ones. |
Yet another meaningless complaint here ... fn execute();
fn execute_with_capacity(cap: usize);
fn execute_with_capacity_and_duration(cap: usize, d: Duration);
fn execute_with_duratino(d: Duration);
fn execute_with_a_and_b_and_c_and_...(carefully:usize,named:usize,repeated:bool,arguments:f64);
// And so on with O(n!) variants ...
... Oh, using struct as parameter? Okay. mod some_deep_place_u_don_no {
pub struct BlahBlahParam {
capacity: usize,
duration: Duration,
}
impl Default for BlahBlahParam {
fn default() -> Self { Self { capacity: 10, duration: Duration::from_millis(100) } }
}
pub fn execute(p: &BlahBlahParam);
}
...
let mut param = some_deep_place_u_don_no::BlahBlahParam::default();
param.capacity = 15;
param.duration = Duration::from_millis(250);
some_deep_place_u_don_no::execute(¶m); Nah no one does use plain struct for that purpose. Using builder pattern is more fancy way to do it! mod other_mod {
#[derive(another_bunch_of_builder_crate_bloats::Builder)]
pub struct AnotherBlahParam { ... }
pub fn another_execute(p: &AnotherBlahParam);
// Or, if u don want dependency bloats, you can of course implement your own builder by
// defining yet another new methods for each respective arguments!
}
other_mod::another_execute(
other_mod::AnotherBlahParamBuilder::default()
.some_value(1)
.another_referenced_param("hello")
.build()
.expect("I forgot something necessary!")
); I agree that trials to reduce verbosity under limited language support helps me to try finding out more concrete and versatile API design(ironically, all these patterns are mostly workaround originated from non-exisistent of this feature), but I just hope there'd be less verbose way to do these daily job :(. The fn execute( positional: usize,
{ capacity: usize, // Must be designated
duration: Duration = Duration::from_millis(100),
tag: impl Into<String> = "", // In default, deduced as '&'static str'
});
execute(25, capacity: 15, duration: Duration::from_millis(123)); |
The way Dart is doing it is pure deliciousness, I hope named arguments are implemented that way. Also the default argument value syntax is so well executed in Dart. It should be taken as inspiration. |
I kind of get on my high-horse about this issue on a regular basis, but the whole "named argument" passing in function calls technique isn't exactly new. Even the first release of the Ada programming language 40 or so years ago, which is still a significant language in the area of safety and mission critical applications included this feature, and still does, and it has always had strong-typing at its heart, so any argument that "strong typing in Rust makes it less necessary" really doesn't seem immensely valid to me. Anyway - I just thought I'd stick my oar in; I'm only just starting to look at Rust more closely, so can't really contribute much technical detail on how this might be implementable, but I have seen some nice things in Rust; I just think this is a significant shortcoming. I've seen similar excuses in, e.g. the Carbon group about how other techniques help work round this sort of thing, but I remain unconvinced that having multiple different ways to do something that ought to be dead easy is a good idea. |
I have created a very basic proc macro parser for follows code. named_args! {
fn f(
arg: u8,
color @ {
r: u8,
g: u8,
b: u8,
//data @ Data { ... }: Data, // unimplemented
}: impl Default,
) {
// output: 0-2-0
println!("{r}-{g}-{b}");
dbg!(arg, color);
}
f(123, @{
g: 2,
..Default::default()
})
}; expanded code: // NOTE: Make sure that the trait has the same name in `Default::default()` and `impl Default`.
fn f(arg: u8, r: u8, g: u8, b: u8) {
// Maybe we can use trait as a `type checker` here?
let color @ (r, g, b) = (r, g, b);
...
} source code: https://github.com/rsuu/rka/blob/main/crates/rka-fn/src/opt/named_args.rs |
UPDATE from 2024. If you are looking for a solution working today I've made a blog post about this problem, that also showcases the crate #[bon::builder]
fn greet(name: &str, age: u32) -> String {
format!("Hello {name} with age {age}!")
}
let greeting = greet()
.name("Bon")
.age(24)
.call();
assert_eq!(greeting, "Hello Bon with age 24!"); |
An entire decade later... I'm curious what is hindering development of this feature today; i.e., what is needed to push it forward? |
Convince people it's more important than other features and should be given priority, or make contribution by your own 😆 |
This issue has the most "thumbs ups" and the most comments by far compared to any other issue in this repo. I am really surprised that a modern language doesn't has this feature. |
That phrasing can be applied to many features that would be an objectively bad fit for Rust. It carries no argumentative weight in and of itself. (eg. As someone who was in /r/rust/ from before Rust v1.0 to when Reddit shut off RSS feeds, it wouldn't surprise me to know that someone familiar with C++ had, at some point, said that about the lack of a global "shut off the borrow checker and make all pointers raw" switch, either "for prototyping", or because they wanted the other language features without the inconvenience of having to learn to write code that's within the checker's ability to prove memory-safe.) |
But it wasn't given by itself! It was given along with the observation that this seems to be a highly desired feature, which makes your example of the make-everything-raw--which isn't even a real example but a postulate about an example that might exist--highly suspect. So the obvious question is then: does your concern have any merit? Has has anyone come up with a compelling example of why any and all of these features are an objectively bad fit for Rust? The answer doesn't need to be the same for the three different features.
I haven't given a fully compelling argument against the latter two here. But it's really hard to see how the named argument part could be un-Rust-like. Exactly how it would work best might be an open question (e.g. should it be based around destructuring of structs; should it be on-by-default or opt-in; etc.). So maybe this is the case. But in 400-odd posts, nobody has managed to give a really clear and compelling argument that would rise to the standard of being an objectively bad fit as opposed to not worth it, attainable enough with macros/libraries, etc.. This suggests at the very least that such an argument, if correct, is not an easy one to find. |
It might be better to split the issue into:
While default values might be complicated because of the ownership rules & traits - named arguments decrease the likelihood of bugs and increase readability without any downsides. A classic example is when you have a function with a lot of flags and numbers. calc(1,5.4,true,2.3,false,false,5) Alternatively, you do it like in JavaScript and pass a temporary struct into the function. This would be a lot of unecessary boilerplate in Rust because declaring a struct is not as short as passing an anonymous object in JavaScript. In short, allowing named arguments makes a language objectively better in terms of readability and avoidance of bugs. Regarding the syntax, I am fine with the Kotlin-way or Dart-way. |
My point was that the phrase can be deleted. Appeal to "modern language" is not a compelling argument. (Until Rust came along, every "modern language" of any significance was garbage collected.)
There's one problem with how you're coming at it... the Rust team doesn't operate on "prove it shouldn't be in here" logic. They operate on "prove there's a compelling enough reason to complicate the language more" logic and, for new syntax, they expect it to be prototyped and iterated on using macros in third-party crates and the demand for it demonstrated in that context.
|
Which is entirely reasonable, and which is why the extensive interest is highly relevant: it indicates that there is something about the feature which is, in principle at least, compelling. Furthermore, although I absolutely agree with you about the differences between e.g. One can use bon, but the fluent builder syntax feels very different than typical function call syntax (very not rust-y), so it's not a great way to explore the space. Right now, if I need named arguments to keep things straight, but don't want to manually build a struct, I'd use that...but grudgingly, not joyously. There was a macro solution that builds macros offered, but that doesn't work well with lambdas. Structural records would have solved the named argument problem an orthogonal way, but that didn't go in either (it, again, is something not easily done with macros/libraries). So you really need to connect the dots all the way in order to have a good point. How, in principle--you don't have to implement it, just sketch out how it could be done--can we get a pretty good, natural-feeling named arguments feature using standard Rust? Because to me, it looks like this is one of the deeper kinds of integrations. You're arguing that there needs to be "prove it's compelling" + "do what you can in libraries but sometimes that's hard", and the interest seems to do the former and the existence of attempts that aren't close to what one would wish for seems to do the latter. What else do you think is needed to at least get to the point where it's worth trying a detailed RFC (with some input from the people who likely would be implementing it, because if they're not going to, there wouldn't be much point). |
My bad. It was late and I agree that I forgot to connect the dots.
Remember that, though it was part of the standard library, TL;DR: The Rust approach is "Except in exceptional circumstances, the language team are like the dictionary. They don't coin new words... they acknowledge popular use of existing ones." (To some extent, this traces back to how, in the early days of Rust when a lot of design bootstrapping was happening, the refrain for choosing what features to include was along the lines of "Rust doesn't try to innovate. Rust gives overlooked good, mature ideas a second chance.") In fact, |
Once again, comments and thumbsups in this repo play no factor whatsoever into the prioritization process. These issues are mostly just for people to talk about things they want to talk about. |
@ssokolow I think you're unlikely to see a popular crate because the language features needed to make it painless aren't present. You have to manually annotate every function, you need a separate name because You can certainly argue people should write such a crate and show some examples of code being clearer but I don't think a crate to hack in something this basic will ever be popular even though people would definitely use the feature if present. You can look at other languages that have the feature with first class support and see that it's very popular, nearly ubiquitous. I don't know anything Rust specific that would change that. It's even popular in Rust in the other contexts where it's allowed, struct constructors and generics. |
So the advice to people who want the feature would be to center usage on one of the existing libraries (and improve it to within what is possible), despite the limitations and syntactic inelegance, in order to demonstrate a gradient-climbing path to what they think is a better optimum? It's not really practical to get as close as |
In practical terms, I don't have a particular preference (while I don't like how its counterpart in Python has encouraged complex APIs like the initializers in LXML, I trust the lang team to be discerning in what they accept) so I'm mainly just talking about what I've observed over the last nine years about how the process works. "Center usage on one of the existing libraries (and improve it to within what is possible), despite the limitations and syntactic inelegance, in order to demonstrate a gradient-climbing path to what they think is a better optimum" is probably the most viable/effective way to influence their perception of priorities if you can't afford to hire people to increase the amount of manpower available to implement RFCs that have already been approved but not yet implemented. |
I think saying But overall, I'd point you to #323 (comment) above -- if using structs is too bulky, the first approach should be to make using structs less bulky if possible, because that will accrue value to existing types too, and in more places. So I would encourage everyone to take a look at #3681, which I like as a nice step in that direction -- though there's certainly more to be done too. Remember, "named parameters" isn't the goal. The goal is making APIs that are ergonomic to write and consume. The current approaches should be made the best they can be before saying that something completely new is needed -- especially when those new things have complex interactions with other existing features. |
I'll repeat what I've stated multiple times in internals: between default field values (#3681) and either inferred types (#3444) or structural records/anonymous fn foo(kargs: _ { foo: i32 = 42, bar: Option<String> = None }) {
} or struct FooArgs {
foo: i32 = 42,
bar: Option<String> = None,
}
fn foo(kargs: FooArgs) {
} both of which can be called with |
Oh, that isn't really bulky. But now we've reserved
Yes, that would help relieve the burden considerably. I have no particular preference for how the problem gets solved. But a safety: maximum mode for function arguments would be nice to have in some way. Perfectly okay if we get there via some other feature.
I would say that if these features are stabilized, then this is how named and possibly optional arguments are implemented. Doing it a second time doesn't carry its weight! But zero times leaves some weight that needs carrying. |
I'll just add here, since I'm still following this issue. I'm learning https://github.com/bevyengine/bevy right now for fun and it has a pretty cool implementation of systems, which can take any number of optional params. https://promethia-27.github.io/dependency_injection_like_bevy_from_scratch/introductions.html I'm not saying this is a solution to the problem, but more like an example of how some creative projects are getting around it. Does nothing for keyword-args, but helps with the rest. |
@estebank, I want to add a perspective that may not be addressed by the RFC's you mentioned (please correct me if I'm wrong). Part of the reason I'd like optional args is for the backwards compatibility benefit. There's a use case where a library prematurely vends a function like:
and then, after it's already in use, the author wants to add configuration options to it like:
If I'm understanding correctly, even with the RFC's you mentioned, that would require a new function like
Playing off that RFC for To summarize, in my ideal world, I can take this:
and turn it into one of the following, without breaking
Which I don't think would be covered by |
Mmm. Interesting thought; takes me back to the early days of Java when every few weeks (it seemed) there would be new versions where compiling your code gave a ton of deprecated function warnings. It would have been interesting to compare that if they'd had the sense to implement named arguments in the first place; the concept wasn't exactly new, even in the early 90s! |
Just throwing the link here as I found this thread. My recently published RFC proposes argument unpacking, which may be somewhat related, since it has interactions with functions at the call site. |
A portion of the community (and of the core team) sees one or more of the following features as important for programmer ergonomics:
This issue is recording that we want to investigate designs for this, but not immediately. The main backwards compatibility concern is about premature commitment to library API's that would be simplified if one adds one or more of the above features. Nonetheless, We believe we can produce a reasonable 1.0 version of Rust without support for this.
(This issue is also going to collect links to all of the useful RFC PR's and/or Rust issues that contain community discussion of these features.)
The text was updated successfully, but these errors were encountered: