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

Comments

Projects
None yet
@pnkfelix
Copy link
Member

pnkfelix 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

flying-sheep 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

pnkfelix commented Oct 9, 2014

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

@reem

This comment has been minimized.

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

This comment has been minimized.

Copy link

flying-sheep 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

Valloric 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

sfackler 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

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

This comment has been minimized.

Copy link

gsingh93 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

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

This comment has been minimized.

Copy link

e-oz 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

steveklabnik commented Dec 4, 2015

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

@pnkfelix

This comment has been minimized.

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

This comment has been minimized.

Copy link

yberreby 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

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

golddranks 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

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

This comment has been minimized.

Copy link

yberreby commented Dec 10, 2015

I agree, macros are appropriate for this.

@reddraggone9

This comment has been minimized.

Copy link

reddraggone9 commented May 24, 2018

@Pauan @Ixrec

Multiple variations of opt-in named args have been mentioned both in this thread and in a related internals discussion linked above. In fact, it looks like making them opt-in was the plan.

I would recommend that anyone thinking about contributing here read both threads. Otherwise we'll just keep retreading old arguments, which doesn't add much to the discussion.

@est31

This comment has been minimized.

Copy link
Contributor

est31 commented May 25, 2018

@varkor yes the IDE argument is very flexible and applicable in multiple directions. You could e.g. argue that let should be replaced by a 100 letter keyword and if someone has problems with that, they should change their IDE to make it appear as let again... But here there is an existing IDE feature and I think it gives us two benefits:

a) you can configure it to display names whenever you want for your use case... I call this IDE induced foveation
b) crate authors can freely change argument names

@bbatha

This comment has been minimized.

Copy link

bbatha commented May 25, 2018

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

This is also true of the builder pattern.

Though I do share your concern about forgetting to specify an optional argument. Perhaps rust should have a syntax like _ in match to denote that you're opting into the remaining defaults.

I feel that named args/optional params/etc introduce a new way of doing things even while the existing way is well enough.

Rust already has two ways to approach this pattern Default + .. and the builder pattern. I'd argue that neither is sufficient and present barriers to entry.

Default is simple enough but has well known limitations (all or nothing).

The builder pattern feels odd and clunky if you come from a background of languages with named parameters. Even if you've seen it before the Rust builder pattern has nuances that don't exist in other languages which hurt its learnability: e.g. which flavor of self should be used in the finalizer?, should it impl Default, would a simple closure be better, etc.?

@bombless

This comment has been minimized.

Copy link

bombless commented May 28, 2018

One way to avoid the chaos named arguments can bring is to only allow named arguments appear on private functions (that is, functions that are not marked as pub and not a trait method). This way, the feature will still be pretty neat, and we can evolve on this path.

@vvuk

This comment has been minimized.

Copy link

vvuk commented May 28, 2018

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented May 29, 2018

is to only allow named arguments appear on private functions

That doesn't solve my complaint with non-opt-in named arguments: that it changes improving a name from a local question (since it can only affect the current function) to a module-level question. That's annoying for both humans and IDEs, and as a result discourages improving names.

@leodasvacas

This comment has been minimized.

Copy link

leodasvacas 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

sighoya 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

est31 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

est31 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

leodasvacas 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

bbatha 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

pirate 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

est31 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

pirate 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

bbatha 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

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

@csharad

This comment has been minimized.

Copy link

csharad 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

Kroc 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

crumblingstatue 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

Ichoran 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
Contributor

Centril 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

crumblingstatue 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

LukasKalbertodt 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

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

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.