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 · 182 comments

Comments

Projects
None yet
@pnkfelix
Copy link
Member

commented Sep 25, 2014

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

This comment has been minimized.

Copy link

commented Oct 8, 2014

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

This comment has been minimized.

Copy link
Member Author

commented Oct 9, 2014

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

@reem

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Oct 9, 2014

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

This comment has been minimized.

Copy link

commented Oct 10, 2014

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

This comment has been minimized.

Copy link
Member

commented Oct 10, 2014

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

@aturon aturon referenced this issue Feb 3, 2015

Closed

Struct Sugar #343

@aturon

This comment has been minimized.

Copy link
Member

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

This comment has been minimized.

Copy link

commented May 18, 2015

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

@aldanor

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Dec 4, 2015

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

@steveklabnik

This comment has been minimized.

Copy link
Member

commented Dec 4, 2015

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

@pnkfelix

This comment has been minimized.

Copy link
Member Author

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

This comment has been minimized.

Copy link

commented Dec 10, 2015

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

commented Dec 10, 2015

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

commented Dec 10, 2015

I agree, macros are appropriate for this.

@leodasvacas

This comment has been minimized.

Copy link

commented Jun 8, 2018

  1. optional arguments hide the fact that you can pass more arguments to the function.

@est31 Thanks for listing out your concerns. This one is in a way crucial to optional args, since they're all about adding args without breaking existing code. But I agree that knowing whether an argument list is or is not exhaustive can be useful information when modifying code.

What would you think of a syntax foo(bar, ..) to indicate that there are optional args being ommitted, and a rustfix-able clippy warning that hints to the foo(bar) to foo(bar, ..) conversion, so that you don't miss it when an optional argument is added?

@sighoya

This comment has been minimized.

Copy link

commented Jun 8, 2018

optional arguments hide the fact that you can pass more arguments to the function.

Or they hide the fact that you can pass fewer arguments at the function call side.
But don't call any function if you don't know its signature and its intention.

I don't know why there is so much hate about optional args and named arguments, they are both syntactic sugar which is resolved by the compiler.

I don't know why this should be a breaking change since functions with default arguments are normal functions with the same arity as before but with the option to omit some arguments at the call side in favor to default settings suggested by the library/function creator. The omitted arguments are then filled up by the compiler.

Whereas named arguments don't change the arity of the function only the struct in which all named args are packed in. Named arguments are better than normal struct parameters in that they allow default values which don't have to be defined in a struct outside the function and which can be easily overdefined without to create a second default struct where only one value of 40 values has changed.

And no, keyword arguments are also type checked.

Optional and named arguments will significantly reduce the misuse of traits in order to achieve variability or optionality.

Variadic arguments are more powerful than arrays in that they can allow for heterogeneous types. Further variadics are some form of generics which allow to parametrize over function signatures.

@est31

This comment has been minimized.

Copy link
Contributor

commented Jun 8, 2018

@leodasvacas yeah foo(arg,..) would fix my point if it were mandatory. Then it'd be the same tradeoff like for struct initializer syntax.

@est31

This comment has been minimized.

Copy link
Contributor

commented Jun 8, 2018

@leodasvacas adding to that: there is surely a tradeoff here of features. I don't think that there is a positive sum game result here, sadly :/.

Another point:

  1. Permutating order means that checking two calls for equivalence turns from a O(n) problem to a O(n^2) problem.
    Easy to check that they are the same:
system.open_valve(id, 1.00, false, true)
system.open_valve(id, 1.00, false, true)

Hard to check:

system.open_valve(id, emit_event_when_open=false, send_drone=true, opening_speed=1.00)
system.open_valve(id, opening_speed=1.00, emit_event_when_open=false, send_drone=true)

Even harder to check (you'd have to check the signature of the function to find out the default for emit_event_when_open):

system.open_valve(id, emit_event_when_open=false, send_drone=true, opening_speed=1.00)
system.open_valve(id, opening_speed=1.00 send_drone=true)

The last example can be fixed by a lint that checks whether you set a named optional arg to the default.

@leodasvacas

This comment has been minimized.

Copy link

commented Jun 8, 2018

@est31 I'm sure rustfmt would be able to order your named arguments alphabetically.

Edit: Also we could require that arguments are used in the same order that they are declared, like Swift does. Python doesn't require this.

@bbatha

This comment has been minimized.

Copy link

commented Jun 8, 2018

Permutating order means that checking two calls for equivalence turns from a O(n) problem to a O(n^2) problem.

If you put the args in a hashmap that's O(n). If you sort them its O(nlogn), for a small amount of arguments, or for the "visual implementation" of the sorting algorithm is sufficient, and is easily assisted by rustfmt as @leodasvacas notes.

@pirate

This comment has been minimized.

Copy link

commented Jun 8, 2018

If omission is allowed but not reordering, you could always just do an O(n) walk over the original function definition to see which arguments are used at each callsite, I don't see why doing an O(n^2) blind comparison between two call sites is ever necessary:

def: system.open_valve(id, opening_speed=1.00, emit_event_when_open=false, send_drone=true, other_val=1.00)

ex1: system.open_valve(id, __________________, emit_event_when_open=false, send_drone=true, other_val=1.00)
ex2: system.open_valve(id, opening_speed=1.00, emit_event_when_open=false, send_drone=true, ______________)
@est31

This comment has been minimized.

Copy link
Contributor

commented Jun 8, 2018

Lol when I mentioned landau O I've originally meant how long it takes for me to compare the lists manually. But yeah if they are sorted such a comparison can be done in O(n). My brain doesn't have O(n) memory available though (at least I want to use it for different things, like reasoning about more interesting things than whether two function invocations are equal) nor do I want to compute hashes in my head so the unsorted case is still in O(n^2).

@pirate

This comment has been minimized.

Copy link

commented Jun 8, 2018

This is not really ever an issue in other languages that have keyword args though, e.g. Python.

@bbatha

This comment has been minimized.

Copy link

commented Jun 8, 2018

long it takes for me to compare the lists manually.

I'd like to point out that this is also a problem in structs where there is no implied ordering of fields, and with the .. syntax you may not even have all of the fields present. Builders also have similar issues, but there's less intent expressed to the compiler, so tooling like rustup can't reorder the optional "params" in a way that's easy to compare. Worse yet with a builder the order of the calls may effect the semantics of the program!

@jgarvin

This comment has been minimized.

Copy link

commented Jul 14, 2018

foo(arg,..) would get rid of the main purpose of the default arguments -- to make it so that you can extend an API by adding new arguments to a function (that previously had all mandatory arguments) without breaking existing callers. The common case for this is that you realize there is some additional aspect of the function's behavior that should be customizable, but where most of the time there is a sensible default (which is usually the original behavior prior to adding the new argument). If you didn't have the foresight to use the builder pattern or whatever special macro people are imagining, right now you're out of luck, and have to come up with a new function that takes the new argument or introduces the builder pattern.

@pepsighan

This comment has been minimized.

Copy link

commented Aug 30, 2018

Since we are brainstorming, I would like to pitch in my thoughts on this. The syntax inspiration is from Dart as well as Swift.

The arguments can be both positional and named & it is backwards compatible.

fn function(a: bool, { b: i32 });
=> function(false, b: 54);

fn function({ a: bool, b: i32 });
=> function(a: false, b: 54);

/// Not allowed. 
fn function({ a: bool }, b: i32);

/// Current syntax still works
fn function(a: bool, b: i32);
=> function(false, 32);

/// Swift like external names for named arguments
fn start({ from start_loc: u32, to end_loc: u32 });
=> start(from: 4, to: 10);

With Default Arguments

fn function({ a: bool = false, b: i32 = 4 });
=> function();
=> function(a: true);
=> function(b: 6);

fn function(a: bool = false);
=> function();
=> function(true);

/// All allowed syntax
fn function({ a: bool, b: i32 = 4 });
fn function({ a: bool = false, b: i32 });
fn function(a: bool = false, b: i32 = 4);
fn function(a: bool, b: i32 = 4);
fn function(b: i32, a: bool = false);
fn function(a: bool, b: i32 = 4, { c: bool });
=> function(true, 5, c: false);
=> function(true, c: false);
fn function(a: bool, b: i32 = 4, { c: bool = true });

The order of the argument names:

  1. Non-optional positional args.
  2. Optional positional args.
  3. Any keyword args.
@Kroc

This comment has been minimized.

Copy link

commented Oct 4, 2018

Wow, I'm a total minority here; I'm actually against this because it involves going to the documentation more often because a function is more "magic" than before. When you stare at a piece of code and you can't tell what it's doing because the function name is very short, but the parameters don't tell you what is happening (a good reason for parameter names, but not optional params).

Optional params bloat such functions with leader code that has to deal with the optional elements, when really this "optional" stuff can be handled in alternate functions that do the preparation, and then call the canonical function.

In every language that has optionals that I've coded in, optionals have not made my code easier to maintain, optimise, or even read. In practice it adds bloat to every such function and reduces readability at the call site. It makes API changes harder because now you have many call sites that have different semantics based on arrity, rather than name. This is no good for search and replace let alone refactoring tools.

Nothing good will come with optionals, but param names and varags should not be conflated as the same beast, these do have purposes and it should be seen that it's OK to reject optionals, but implement param names / varargs.

@crumblingstatue

This comment has been minimized.

Copy link

commented Oct 4, 2018

I'm actually against this because it involves going to the documentation more often because a function is more "magic" than before. When you stare at a piece of code and you can't tell what it's doing because the function name is very short, but the parameters don't tell you what is happening (a good reason for parameter names, but not optional params).

Sure, it can be abused, like most language features, but that doesn't mean it doesn't have valid use cases.

Consider the constructor of RenderWindow from SFML. (note that I'm a maintainer of rust-sfml, so I might be biased here)

sf::RenderWindow win(sf::VideoMode(640, 480), "My Application");

It is pretty clear that this creates a RenderWindow with 640x480 resolution and the title "My Application". You don't even need to look at the documentation to figure out what it does. All the defaults for the optional args are very sensible. In fact, they literally call the default constructors of their respective types. The only exception is VideoMode, which provides a very sensible default bit depth of 32.

Now let's consider the alternatives in Rust.

1. Explicit arguments

Do nothing fancy, just require all arguments to be explicit.

let win = RenderWindow::new(VideoMode::new(640, 480, 32), "My Application", Style::default(), ContextSettings::default());

This definitely has the advantage of being explicit about everything, so there can be no misunderstandings. However, the extra explicit arguments given are all defaults. If they weren't explicitly provided, it would be very sensible to assume that the function uses the defaults.

What's the disadvantage? Well, simply put, it's annoying for the user to always have to explicitly provide defaults. If there are a lot of functions like this, it can wear the user down, and make them annoyed with the API. "Why do I always have to say (..., Foo::default(), Bar::default(), ...)? All I want is a window of this size and this title. This is annoying."

Although I do acknowledge, that a "serious" systems programming language might not want to prioritize convenience over explicitness. But Rust can be, and is used for developing applications, games, etc.. If we want to compete with all areas of C++, we might want to consider the convenience features that C++ provides.

2. Different functions for implicit/explicit args

let win = RenderWindow::new(VideoMode::new(640, 480, 32), "My Application");

let win = RenderWindow::new_ext(VideoMode::new(640, 480, 32), "My Application", Style::Fullscreen, ContextSettings::new(...));

Now we have two functions that essentially do the same thing, only one provides sensible defaults, the other is explicit.

This has no advantage over defaulted params. RenderWindow::new does the same "magic" as it would with optional params. Only that there are now 2 function names the user needs to remember. 4 if you wanted to make a different function for all combinations of optional args. Also the required arguments are all repeated in the type signatures for all the different functions. Not a very DRY approach.

3. Builder pattern

This is the most commonly used "substitute" for optional args.

The usage would look like this:

let win = RenderWindowBuilder::new(VideoMode::new(640, 480, 32), "My Application").build();

let win = RenderWindowBuilder::new(VideoMode::new(640, 480, 32), "My Application").style(Style::Fullscreen).context_settings(ContextSettings::new(...)).build();

The advantage here is that it's explicit that this is a "builder", so it has optional args. No "surprise" optional args.

However, it has several disadvantages. The biggest one is API inflation. Now you have to have a RenderWindowBuilder in addition to RenderWindow, and builders everywhere where you wanted to have optional arguments. This leads to a very messy API if you have a lot of optional arguments. Note that I didn't even make a builder for VideoMode. It would be very ugly to do that just for one optional argument.

It also makes the default case uglier. Now instead of just simply calling a function, the user has to create a builder, and call .build() on it.

This would also look very ugly and unintuitive for functions that aren't constructors. Consider sf::RenderTarget::clear.

The default usage is win.clear(); // clears with black color, sensible default or win.clear(desired_color);.

What would this look like with builders? win.clear_builder().color(desired_color).clear();?

4. Fancy magic with Rust generics

You can do fancy things with Rust traits. For example, you could implement Into<RenderWindow> for different tuples. This is a poor man's version of optional args. It has all the "disadvantages" of optional args, and several more.

The API becomes harder to understand due to all the different generic types involved. The user can't look up the usage in one single place. They have to look for what implements Into<RenderWindow>.

Compile times can potentially suffer, but probably not by much.

And finally, generic methods can't be called on trait objects, so this solution can't work in any context where dynamic dispatch is required.

Addressing the rest of the arguments

Optional params bloat such functions with leader code that has to deal with the optional elements, when really this "optional" stuff can be handled in alternate functions that do the preparation, and then call the canonical function.

This isn't a problem if it's kept simple, like with SFML in the above examples.

In practice it adds bloat to every such function and reduces readability at the call site.

In the above examples, the alternate APIs had more "bloat" than an API with language-level optional args would. Again, readability is not a problem if the provided defaults are sensible.

It makes API changes harder because now you have many call sites that have different semantics based on arrity, rather than name. This is no good for search and replace let alone refactoring tools.

This is also true for many uses of macros and generics, which Rust supports in spite of this.

@Ichoran

This comment has been minimized.

Copy link

commented Oct 5, 2018

I would like to see default arguments have the same level of support as default copying of structs. After all, an argument list is isomorphic to a struct containing all the arguments. The one wrinkle is that you only want to give a Default trait to part of the list, maybe not all of it. Reasoning this through, it suggests that

  1. Any unspecified default arguments have to be summoned with ..
  2. Arguments can be specified out of order if you specify them by name (but you must specify all of them by name if you do it that way)
  3. The defaultable arguments must always be at the end of the argument list so the division is clear (debatable, but the easiest way to handle "part of the list")
  4. The defaultable arguments look exactly like a struct, i.e. fn example(a: i32, { b: bool, s: String })

This doesn't suggest an obvious way to provide the defaults, however. The closest analog to an impl section that is specific to a function is its where clause, so perhaps it could go there. Alternatively, one could use { b: bool = true-type syntax, but then this should be allowed for deriving struct defaults also.

It also doesn't address whether default arguments should be constants or whether they can be computed from the non-default arguments. Constants are less surprising, but computed defaults can be extremely useful in cases where you would other have to use ad-hoc sentinel values (e.g. if the default should be to match the length of an input).

@Centril

This comment has been minimized.

Copy link
Member

commented Nov 2, 2018

I've proposed structural records (#2584), it's not exactly named function arguments (so I'm keeping this issue open), but it overlaps a bit...

@crumblingstatue

This comment has been minimized.

Copy link

commented Nov 2, 2018

I've proposed structural records (#2584), it's not exactly named function arguments (so I'm keeping this issue open), but it overlaps a bit...

Keep in mind that this issue is not just about named args, but also about optional args.

@LukasKalbertodt

This comment has been minimized.

Copy link
Contributor

commented Nov 2, 2018

Keep in mind that this issue is not just about named args, but also about optional args.

And "variable-arity functions" which probably includes the huge topic of variadic generics. Considering this and the number of comments here, I wonder if this issue should be split into multiple ones.

@crumblingstatue

This comment has been minimized.

Copy link

commented Nov 4, 2018

For those interested in these features, check out #1806 (comment)!

Using #2584 in conjunction with #1806 could provide a solution that I believe would cover most of the use cases for named/default args.

@tzachshabtay

This comment has been minimized.

Copy link

commented May 18, 2019

Not sure if this point has already been raised, but I didn't see it:

For me, rust is all about eradicating classes of issues that plague other languages.
If we have named arguments, we could add a feature to the compiler to eradicate a very common programming bug:

fn doSomething(u8 x, u8 y) {...}

doSomething(y, x); //oh oh, the variables are in the wrong order!

If we have named arguments, we can make the compiler not allow positional arguments in the cases where you have 2 arguments with the same type, i.e the compiler will force you to write:

doSomething(x: x, y: y);

So this bug will never happen again.

@berkus

This comment has been minimized.

Copy link

commented May 20, 2019

@tzachshabtay even better, since in rust struct fields with matching names can and should drop the field: field syntax, this:

doSomething(x: x, y: y);

becomes this:

doSomething(x, y);

while STILL KEEPING the compile time check that you used the named arguments correctly. WOW.

@jean-airoldie

This comment has been minimized.

Copy link

commented May 20, 2019

@berkus However this would mean that changing the name of the fields of a function call would be a breaking change.

@berkus

This comment has been minimized.

Copy link

commented May 20, 2019

Yes, for this the approach taken by Swift lang could be better usable. This thing had been thought out there.

@xiaoniu-578fa6bff964d005

This comment has been minimized.

Copy link

commented Jun 16, 2019

Seems many people agree that macros are appropriate for this task, yet I haven't seen a crate to generate such a macro.

So I wrote a proc_macro duang to implement it.

For example:

duang! ( fn add(a: i32 = 1, b: i32 = 2) -> i32 { a + b } );
fn main() {
    assert_eq!(add!(b=3, a=4), 7);
    assert_eq!(add!(6), 8);
    assert_eq!(add(4,5), 9);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.