Skip to content
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

Open
pnkfelix opened this issue Sep 25, 2014 · 417 comments
Labels
A-expressions Term language related proposals & ideas A-syntax Syntax related proposals & ideas A-typesystem Type system related proposals & ideas postponed RFCs that have been postponed and may be revisited at a later time. T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@pnkfelix
Copy link
Member

A portion of the community (and of the core team) sees one or more of the following features as important for programmer ergonomics:

  • keyword-based parameters (as opposed to position-based parameters),
  • optional parameters (where one provides a default value for them, usually in the parameter list),
  • variable-arity functions (which can be seen as a generalization or variation on optional parameters, depending on how you look at it).

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.)

@flying-sheep
Copy link

Nonetheless, We believe we produce a reasonable 1.0 version of Rust without support for this.

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 ImmutableSlice:

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];

slice_from and slice_to will be immediately obsolete once you can just leave out start or end from slice. i bet there are hundreds of examples more.

@pnkfelix
Copy link
Member Author

pnkfelix commented Oct 9, 2014

@flying-sheep so then you deprecate such methods, just like today, no?

@reem
Copy link

reem commented Oct 9, 2014

@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.

@flying-sheep
Copy link

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)

@Valloric
Copy link

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.

@sfackler
Copy link
Member

Rustdoc's handling of deprecated items definitely needs some improvement - see rust-lang/rust#15468 for some discussion.

@aturon aturon mentioned this issue Feb 3, 2015
@aturon
Copy link
Member

aturon commented Feb 3, 2015

See the "Struct sugar" RFC for another take.

@pnkfelix pnkfelix changed the title Wishlist: functions with keyword args, optional args, and/or variable-arity argument lists Wishlist: functions with keyword args, optional args, and/or variable-arity argument (varargs) lists Mar 11, 2015
@gsingh93
Copy link

I'd like to see some of these RFCs revived in the near future, if someone has time to do so.

@aldanor
Copy link

aldanor commented May 23, 2015

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.

@e-oz
Copy link

e-oz commented Dec 4, 2015

Ok, 1.0 released, even more, can we please discuss it again? especially default arguments.

@steveklabnik
Copy link
Member

This issue is open, it's free to discuss.

@pnkfelix
Copy link
Member Author

pnkfelix commented Dec 4, 2015

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...)

@yberreby
Copy link

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?

@ticki
Copy link
Contributor

ticki commented Dec 10, 2015

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)

@ticki
Copy link
Contributor

ticki commented Dec 10, 2015

Also, this feature could easily be misused by taking named parameters instead of structs, which, I think, is a bad thing.

@golddranks
Copy link

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.

@ticki
Copy link
Contributor

ticki commented Dec 10, 2015

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.

@yberreby
Copy link

I agree, macros are appropriate for this.

@robinmoussu
Copy link

Thus my takeaway here is that Rust should first try fixing the "whole bunch of manual work" part first -- after all, that'd help everyone using structs in Rust, not just function parameters. And if passing a struct is a good idea even if named parameters exist in the language, then we should absolutely make it easier.

@scottmcm Is there some writing to explain why they did deprecate the named argument version by a strong type? Because personally I don’t understand why.

@bionicles
Copy link

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.

@damymetzke
Copy link

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.

@svenjacobs
Copy link

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.

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.

@MatrixDev
Copy link

Maybe have a look at Kotlin for inspiration

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.

@kang-sw
Copy link

kang-sw commented Jul 30, 2023

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(&param);

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 Dart syntax that above comment suggests would definitely be sweet sugar!

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));

@KristijanZic
Copy link

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.

@jmccabe
Copy link

jmccabe commented Nov 29, 2023

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.

@rsuu
Copy link

rsuu commented Feb 27, 2024

I have created a very basic proc macro parser for follows code.
And I think it is easy to implement in rustc.

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
examples: https://github.com/rsuu/rka/blob/main/crates/rka/examples/named_args.rs

@Veetaha
Copy link

Veetaha commented Jul 30, 2024

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 that can be used to solve this problem by generating a builder for a function:

#[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!");

@SmartBoy84
Copy link

An entire decade later... I'm curious what is hindering development of this feature today; i.e., what is needed to push it forward?

@slanterns
Copy link
Contributor

slanterns commented Jul 31, 2024

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 😆

@Tienisto
Copy link

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.

@ssokolow
Copy link

ssokolow commented Aug 28, 2024

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.)

@Ichoran
Copy link

Ichoran commented Aug 28, 2024

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.

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.

  • Named arguments can be an important safety feature when too many positional arguments have the same type. None of the workarounds seem adequately ergonomic; if they were, they would be "named arguments in effect if not in reality". This fits well with Rust's focus on safety: don't make the safe thing to do the hard thing to do. There are very few instances where I find using Scala a relief instead of Rust from a safety perspective. This is one of them.

  • Optional arguments are for convenience but can introduce uncertainty about what is actually being passed where. Because Rust does not have overloading for functions, and this essentially implements overloading for functions of a particular limited type, it's a dubious proposal despite the usability advantages. However, we do already have optional arguments for structs, so there's prior art that as long as it's clear what you're doing, you don't have to spell out every single piece every single time.

  • Vararg arguments are also for convenience and have the added difficulty of obscuring a resource management issue and/or raising a scalability issue: you actually need your varargs to go into a fixed-size array or Vec or something like that. But this means you're going to need to think about how it ends up in memory, and that in turn means that the Rust approach is to make it explicit and thereby under the programmer's control. So this seems even more dubious than optional arguments.

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.

@Tienisto
Copy link

It might be better to split the issue into:

  1. Named arguments
  2. Optional arguments with default value

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.

@ssokolow
Copy link

ssokolow commented Aug 28, 2024

But it wasn't given by itself!

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.)

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.

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.

  • That's why they just finished merging a derivative of the once_cell crate into the standard library. ...because every non-trivial project has wound up, whether directly or indirectly, depending on either lazy_static or once_cell or often both. (That's also a good example in that lazy_static came first and once_cell was a v2.0 take on meeting the need. If they'd merged lazy_static, we'd have been stuck with an inferior API.)
  • That's why we don't have delegation (A.K.A. A rust-y take on implementation inheritance) in the language yet. They're still watching to see how crates like delegate evolve, both in design and user uptake.
  • I can't remember whether it was a previous revision of Too Many Lists, but I remember one dev lamenting that std::collections::LinkedList is in the standard library because they ran out of time to argue for its removal before the v1.0 freeze.
  • Why is async/await part of the language itself instead of a macro in a third-party crate like futures? ...because holding borrows across await points requires something built deeply enough into the language for the borrow checker to understand it.
  • Why is std::future::Future in the standard library while the http crate isn't? Because the definition of async and await reference Future, while the standard HTTP types are not depended on by anything in the standard library.

@Ichoran
Copy link

Ichoran commented Aug 28, 2024

They operate on "prove there's a compelling enough reason to complicate the language more" logic

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. once_cell and async/await, you didn't make an argument that named arguments are more like the former than the latter. You can't use regular Rust features to get named arguments, so to me it looks a lot more like the latter.

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).

@ssokolow
Copy link

ssokolow commented Aug 28, 2024

Furthermore, although I absolutely agree with you about the differences between e.g. once_cell and async/await, you didn't make an argument that named arguments are more like the former than the latter. You can't use regular Rust features to get named arguments, so to me it looks a lot more like the latter.

My bad. It was late and I agree that I forgot to connect the dots.

  1. Argument from precedent: Part of what's holding up delegation is that, because there are multiple ways to implement it and no one set of trade-offs that's objectively superior, one of the criteria they gave when voting to wait and watch was to see if a single design wins out in the wild significantly enough to merit privileging it and, if so, which one.
  2. Argument from consensus: There's an entire named-arguments keyword, not just one implementation everyone's using, and there isn't one that's exponentially more popular than the others.
  3. Argument from observed demand: If you ignore the "proc macro crate. Use X instead." entries, there are 12 crates with that keyword and, according to lib.rs, not a single one of them shows monthly downloads that exceed double digits. That means, at its most favourable generalization, demand for crates implementing this can't even break 1200 monthly downloads. (Compare once_cell and lazy_static which have 13 million and 12 million monthly downloads, respectively.)

Remember that, though it was part of the standard library, ? began as a macro named try! before getting promoted to a new piece of syntax. Again, precedent for things proving themselves as macros before being considered for new syntax.

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, async/await isn't that much of an exception, because the futures crate used to be interacted with via macros and it proved itself too inconvenient to not have special-case borrow checker integration.

@steveklabnik
Copy link
Member

This issue has the most "thumbs ups" and the most comments by far compared to any other issue in this repo.

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.

@jgarvin
Copy link

jgarvin commented Aug 28, 2024

@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 ! isn't part of the name, users of your library might end up having to learn how multiple keyword argument crates work etc.

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.

@Ichoran
Copy link

Ichoran commented Aug 28, 2024

Remember that, though it was part of the standard library, ? began as a macro named try! before getting promoted to a new piece of syntax.

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 try!() using existing language features, at least if the various attempts are anything to go by, but it might possibly be close enough to be enough of an improvement to manual struct creation as the "solution". When the point of the feature is to make it easy to do the safe thing, language limitations that make library solutions less-easy isn't a very good proving ground. For instance, if the complaint is "having to say foo(FooArgs{ start: 7, length: 4, skip: 0 }) is too onerous for regular use", foo(instruct!{ start: 7, length: 4, skip: 0 }) isn't that amazing of an improvement--it's nice to not have to remember the struct name, but it's still really bulky.

@ssokolow
Copy link

ssokolow commented Aug 28, 2024

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.

@scottmcm
Copy link
Member

scottmcm commented Aug 28, 2024

foo(instruct!{ start: 7, length: 4, skip: 0 }) isn't that amazing of an improvement--it's nice to not have to remember the struct name, but it's still really bulky.

I think saying foo(a!{ start: 7, length: 4, skip: 0 }) is "really bulky" compared to foo(start: 7, length: 4, skip: 0) is overstated. It's already substantially less than ..Default::default() for example -- and that macro works today.

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.

@estebank
Copy link
Contributor

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 structs (#2584) you can end up with

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 foo(_ { bar: Some(s), ..}) (do not focus entirely on the syntax, focus on the semantics). If any of these features are stabilized, then I do not believe that named and optional arguments carry their weight, particularly when you need to get into the defaults of what their ABI is like.

@Ichoran
Copy link

Ichoran commented Aug 28, 2024

I think saying foo(a!{ start: 7, length: 4, skip: 0 }) is "really bulky" compared to foo(start: 7, length: 4, skip: 0) is overstated

Oh, that isn't really bulky. But now we've reserved a as a macro identifier. Are we allowed? Honestly, that is what I'd do if I really needed the feature--when it's important to reduce bulk, I consume as much of the short-identifier namespace as I need. But you can't do that with very many things at once.

#3681, which I like as a nice step in that direction

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.

If any of these features are stabilized, then I do not believe that named and optional arguments carry their weight

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.

@nixpulvis
Copy link

nixpulvis commented Aug 28, 2024

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.

@mlaota
Copy link

mlaota commented Aug 30, 2024

@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:

fn foo() {}

and then, after it's already in use, the author wants to add configuration options to it like:

fn foo(flag_one: bool = true, flag_two: bool = false) {}

If I'm understanding correctly, even with the RFC's you mentioned, that would require a new function like foo_with_flags to be created because that decision to support kwargs wasn't made upfront (this would impact basically any function that predates the default_field_values feature and wants to add configuration/args with defaults). Bridging the gap with kwarg structs + defaults may encourage authors to defensively create "just-in-case" parameters with empty structs resulting in a lot of:

let result = foo({});

Playing off that RFC for default_field_values, a workaround might be to infer the default of a missing last arg iff the arg is a struct that implements Default, but I don't know the feasibility of something like that. It might end up being more difficult than supporting named arguments with defaults

To summarize, in my ideal world, I can take this:

fn echo(msg: &str) -> String { ... }

let echoed = echo("hello")

and turn it into one of the following, without breaking echo("hello"):

// Option 1

fn echo(msg: &str, capitalize = false) -> String { ... }
// Option 2

#[derive(Default)]
struct EchoConfig {
  capitalize: bool = false
}
// note: I used "?" as shorthand to specify Option<EchoConfig>
fn echo(msg: &str, cfg?: EchoConfig) -> String { ... }

Which I don't think would be covered by default_field_values without a breaking change.

@jmccabe
Copy link

jmccabe commented Aug 31, 2024

@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:...

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-expressions Term language related proposals & ideas A-syntax Syntax related proposals & ideas A-typesystem Type system related proposals & ideas postponed RFCs that have been postponed and may be revisited at a later time. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests