I agree that anonymous structures were basically solve this, except for two things. We would need the ability to specify defaults "inline" and calling default::Default()...
is very long.
I do understand the concern that changing the API for the stdlib is not really practical.
At least for me personally, the big win with named/optional parameters is not for stdlib or widely used public APIs, but for internal functions in an application or library which are used at a few places. For widely used APIs doing a builder pattern or similar mechanism is worthwhile. For the typically large number of functions which are only used at a few places, I end up with an uglier API.
I believe that the amount of already existing Rust code is a tiny fraction of code which is yet to be written. Hence my hope that named/optional arguments will happen soon
Another matter for consideration here...
I don't mind named arguments, and they're often improving things at the usage site, but I have never enjoyed writing them. Right now, writing function signatures is basically a breeze, because you just don't have that much to consider, and the easiest way forward is often the correct way forward. But with named arguments, it's much easier to worry that you're doing something wrong if you're not using them. And I'm not looking forward to other people thinking that way and rewriting already fine code to use a new API just because it's more 'modern', and everyone else having to decide up front which API they prefer to use.
Maybe this is just my preference, but if you're going to give me options, I want those options to be available for controlling the runtime behavior of my code, not just different ways to make it appealing in various subjective ways, because even my own subjective opinion of what looks best changes from month to month.
So even though I think it looks better, I wouldn't want it to be a priority.
In addition to this there is something else I never see brought up in these discussions.
Rust functions do not have argument names, they have argument patterns. So it would be necessary to specify an external argument name anyway.
Not necessarily. We could have a #[named]
attribute for functions that allow named arguments, and the compiler can verify that functions with this attribute have exactly one variable binding per parameter:
#[named]
fn function(foo: u8, &bar: &usize) {}
function(foo = 4, bar = &2);
Nitpick: You're right when it comes to Objective-C, but Swift actually does support default arguments. You only need
func createFile(path: String, mode: Int = 42) {}
and you can call it both with and without the defaulted argument:
createFile(path: "asdf", mode: "qwer")
createFile(path: "asdf")
Swift also supports arbitrary function overloading as long as at least one argument has either a different name or a different type. It's a very magic-indulgent language
Of course, Rust doesn't need to go that far...
That still seems like pure syntax sugar, an optional argument could be implemented as generating a wrapper function that delegates to the fully defined function with the default filled in.
Unless you can pass around a function reference that has an optional argument, and dynamically call it with/without the optional argument?
A fun idea that I don't recall hearing suggested before is desugaring a named argument call to an invocation of a builder/options object. If we did that, then we could introduce it in a backwards compatible way into the stdlib for any options objects (like open file). I'm thinking a trait that defines two function: required arguments to new builder instance, and the second would be the action function which consumes the function.
As I think about it, it's probably because it would still be a lot of pain to define said builder struct, and automatically deriving one from a function declaration might be too magical.
Edit: There's also the issue of when you commit named arguments entirely on an function that has them, the compiler would (probably?) need sort of hint that you're trying to invoke the named-argument style desugaring instead of a normal function call.
It did occur to me that you can do a lot of macro magic to make named arguments desugar to other stuff, but the fact that I've never heard of anyone actually using such a macro (and no one has mentioned any crates for it in this thread yet) strongly implies that the demand for named-params-as-sugar is just not there. And at least to me, that suggests there's not really that much demand for a post-1.0 bolted-on named params feature, as opposed to the first-class-from-day-one named params we obviously can't do at this point. I just didn't want to make that point in my earlier posts since it's such a weak and speculative argument.
Such macros would degrade IDE experience. Furthermore, you don't need to desugar named arguments, because the builder pattern is already ergonomic and expressive enough.
The main problem with the builder pattern is not the usage but the creation of the builder pattern. It's a lot of boilerplate, and there are many crates with macros to simplify it. Named and optional arguments would solve this problem much more nicely.
Note that named and even optional arguments could be very efficient. When you have a function such as
fn foo(a: i32 = 0, b: bool, c: Option<String> = None) {}
Then calling foo(b = true)
could be desugared to foo(0, true, None)
.
Before moving mostly to Rust, I've programmed in Kotlin and Python, both of which make heavy use of named arguments. I though that I would miss named arguments a lot, but, surprisingly, this is not the case. For sure, I can make use of this feature occasionally, but this is pretty rare and never a deal-breaker.
It might be the case that my programming style as a whole shifted. In Python I usually program by building small DSLs, I want the call site to be concise and aesthetically pleasing. When using Rust, I subjectively value the prettiness and terseness of the code much less. Rather, I aim at the right runtime behavior (single pass over data, no allocations, etc).
Strongly agree.
Coming from D, a big problem was that multiple proposals for named arguments were submitted, but the proposals just described the author's favorite option for various trade-offs, and there was never work analyzing these trade-offs and wondering why people have different opinions about them in the first place.
My takeaway here is that there exist many arguments to clean up the code with named arguments, many existing proposals overlap with this target, but only partially and focus mainly on syntax sugar. So they don't push programmers to write better readable interfaces. While it's Rust ideal to push programmers to write code which doesn't messes up their memory, write more functional code with less side effects, etc.. Some people can and will always ignore the major directions and program always as if they never stopped programming C++, but that certainly isn't the majority?!
Clearer proposal:
-
Copy Swift's named arguments. As the Swift creators told, they copied from Rust (and other languages). There are already many design similiarities, I see no conflicts copying the named arguments template from Swift, fitting it to Rust where necessary. And the Swift solution has already been tweaked till it was considered to work best.
-
Apply the "Named, non-optional, fixed order arguments" to whole new crates only. With this strategy newly written crates are using named arguments, but old crates don't get a confusing mix of old and new.
If it's true that most code for Rust must still be written (very likely), in the near future unnamed argument functions will become an unimportant nuisance and the named arguments the norm. It should reduce the learning curve for beginners and even experts, every time they use a new library with a new set of functions and their parameters.
This is my main concern, eternal discussions which are very interesting but fruitless. What can I do to push this forward?
Make a demo fork of the compiler (without unit tests, etc., just as proof-of-concept)? Would that help?
I'm no authority on what the lang team will or won't actually accept, but I don't think anyone's ever suggested that implementation difficulty or lack of resources to implement is the key issue here, so I highly doubt a PR would accomplish anything. The issue is a lack of consensus that we want any form of named arguments in the language, much less which kind (again, full disclosure: I still think we don't).
And that is literally why the RFC process exists. If this is ever going to happen, it's going to be because someone wrote a thorough RFC that covers all of the pros, cons, and alternative design choices with detailed reasoning behind why this specific design is better than any of the other things we could do (including doing nothing, or implementing structural records instead, etc), and people are actually convinced by the reasoning it presents. And yes, that actually can be done.
Maybe the "clearer proposal" in your post is on to something, but it needs way more fleshing out to be a complete proposal we could meaningfully respond to. I won't spam you with questions myself since, if you've read the rest of this thread, you should already know most of them and be sick to death of hearing them (EDIT: okay, CAD spelled out some of them anyway, lol)
Big detail you've ignored here: how do you facilitate using a "named arguments edition" crate from a "no named arguments edition" crate?
(I believe one of the existing edition requirements is that it should be possible to write code that compiles without warnings on two adjacent editions, so a wholesale switch based on edition wouldn't be allowed per that guideline. But for the sake of the above question, I assume that's a solved problem via using rustfix to update and a system like Swift/ObjC to provide both the current "no named arguments" and a "named arguments edition" view of std.)
The easy example here is, again, HashMap
. Currently, we have the construction functions HashMap::new()
, HashMap::with_capacity(usize)
, HashMap::with_hasher(impl BuildHasher)
, and HashMap::with_capacity_and_hasher(usize, impl BuildHasher)
.
With named arguments, I'd poentially expose these as HashMap::new()
, HashMap::with(capacity:usize)
, HashMap::with(hasher:impl BuildHasher)
, and HashMap::with(capacity:usize, hasher:impl BuildHasher)
.
If I write that API on edition20XX with named arguments, the crate still needs to be usable from edition2015 and edition2018.
You could just require me to provide the with_capacity_and_hasher
name, but that's basically requiring everyone to write backcompat unique names that (trending towards) nobody will use, because for some reason the compiler doesn't use name mangling to handle that case? (From the POV of 20XX where nobody really uses old editions anymore.)
This sounds perfectly fine if you don't care about current editions using future editions' crates (they should just upgrade!), but Rust does care about that.
That's a real problem, which Apple had to solve, because you can call Swift and Objective-C methods from each other. While Swift evolved from Objective-C, it has many differences (e.g. because of Rust), so the method signatures differ a lot. But there is a fixed pattern how Swift Methods appear in Objective-C and Objective-C methods appear in Swift, they tweaked that a few times, but I think they settled that and are happy with the result. I see little problems using that as a template how to do this in Rust to ensure compatibility.
But that's a really good argument that should be solved in detail how exactly those translations work and written into a RFC for named arguments.
FWIW, I am on the fence when it comes to named arguments: sure, they are an obvious way to make code read nicer, but they also have the tendency to paper over bad design.
If you have a function call like obj.execute(47, true, false, true)
that doesn't immediately tell you what each parameter does, is a clear code smell to me, indicating that the API needs improvement.
While with named parameter that can become vastly more readable obj.execute(timeout_ms=47, error_on_timeout=true, auto_join=false, use_default_executor=true)
, the interface still is essentially the same, but now likely will be considered "good enough".
Granted, real life usually prevents endless tinkering and refactoring to arrive at an ideal API, but named parameters tend to shorten that process even further, esp. with optional parameters that can be endlessly tacked on the end of a function without immediately breaking things.
This really looks bad. But the direction of Swift is more like
job.calc_with_timeout(47, error_on_timeout: true, auto_join: false,...)
. And the order of the parameters is not optional. But arguments can be optional in Swift, that's true.
A better solution would be to either don't allow optional parameters at all, or just the last ones, so that there is no alternating between optional and non-optional.
After considerations and thinking much about if the RFC is "prime time" ready I tought: Just release it as a draft. And so: Here it is. A draft of the Named, Non-Optional, Fixed order arguments: https://github.com/JanDiederich/rfcs/blob/master/0000-named-fixed-order-arguments.md.
Please: Give me your input, what flaws and missing parts do you see in this draft? Don't hold back!
I would like to highlight the "pub argument blocks" (PAB) proposal, which I think was not mentioned in this thread: