rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.97k stars 1.57k forks source link

Wishlist: functions with keyword args, optional args, and/or variable-arity argument (varargs) lists #323

Open pnkfelix opened 10 years ago

pnkfelix commented 10 years ago

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

This issue is recording that we want to investigate designs for this, but not immediately. The main backwards compatibility concern is about premature commitment to library API's that would be simplified if one adds one or more of the above features. Nonetheless, We believe we can produce a reasonable 1.0 version of Rust without support for this.

(This issue is also going to collect links to all of the useful RFC PR's and/or Rust issues that contain community discussion of these features.)

Pauan commented 6 years ago

@Keats It's not about builders: if named arguments were added it would affect all functions.

For example, during the prototyping stage you might have a function with an argument called a, and then you want to rename it to a better name like price.

Or if you change the function's internal implementation then you would want to adjust the parameter names to match.

As @est31 said, people currently take advantage of that to rename function arguments in non-breaking ways, but they can't do that if named arguments were added.

You view them as being a part of the type because that's how it is in some other languages (like Python), but that's not how it is in Rust. So it would be a big change to the entire ecosystem.

There might be ways to avoid that particular problem: for example by requiring a #[keyword_arguments] attribute, so that way if you don't use that attribute then you can rename the function arguments in a non-breaking way.

To be clear, I'm not against named arguments, I'm just clarifying facts so that the decision can be made with the most information.

Ixrec commented 6 years ago

I feel like some commenters are assuming that "named arguments" must mean all arguments to all functions have a name exposed that callers are allowed to use, and other commenters are assuming that exposing your names to callers is opt-in per-argument, and thus a lot of us are talking past each other.

Personally, I like the idea of named arguments, and I haven't thought about them all that much, but I do think they'd have to be something the function author opts in to in order for them to be a net win for Rust (or probably any language), especially since we didn't have them at 1.0.

reddraggone9 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

Another possible option would be to both introduce language syntax for named arguments to distinguish them from positional ones (@name?) and require that the names be used at call sites.

scottmcm commented 6 years ago

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.

leoyvens commented 6 years ago
  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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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

leoyvens commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

bbatha commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

I've proposed structural records (https://github.com/rust-lang/rfcs/pull/2584), it's not exactly named function arguments (so I'm keeping this issue open), but it overlaps a bit...

crumblingstatue commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

For those interested in these features, check out https://github.com/rust-lang/rfcs/pull/1806#issuecomment-435680429!

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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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

berkus commented 5 years ago

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

xiaoniu-578fa6bff964d005 commented 5 years ago

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);
}
ghost commented 5 years ago

Personally, I would be strongly against optional/named arguments for a language such as Rust. It's a feature that belongs in the space of scripting languages such as python. You can simply use an existing datastructure:

fn optional_arg(optional: Option<i32>) -> i32 {
    match optional {
        Some(a) => a + 1,
        None => 0
    }
}

let answer = optional_arg(Some(1));
let answer_default = optional_arg(None);
dbg!(answer, answer_default);

(I should point out in fairness that my example uses dbg! with optional arguments—conceded, that's what macros are for! It's about having a hygienic place for these sorts of language features.)

@crumblingstatue Functions with many unnamed arguments are discouraged, just like how tuples/structs with many unnamed fields are discouraged. Instead, you should use Rust's existing datastructure for named arguments: a struct. Then you could make default constructors that allow users to overwrite fields with Rust's struct update syntax.

. . .  // After `RenderWindow`'s implementation

struct Window<'a> {
    resolution: VideoMode,
    title: &'a str,
    style: Style,
    settings: ContextSettings,
}
impl<'a> Window<'a> {
    // Non-optional values in method signature
    fn new(vm: VideoMode, s: &'a str) -> Self {
        // optional values in returned struct
        Window {
            resolution: vm,
            title: s,
            style: Style::Fullscreen,         // <-- default!
            settings: ContextSettings::new(), // <-- default also!
        }
    }
}

fn main() {
    let win = Window {
        style: Style::Windowed, // Overwrite default to set an alternative preference
        ..Window::new(VideoMode::new(640, 480, 32), "My Application")
    };
    let win_rendered = RenderWindow::new(win);

    . . .  // more things with `win_rendered`
}

You can say that this is more verbose, and yeah, the way I've written it is. However—I consider it an advantage that the language lends itself to good API design; Creating the Window struct allows end users to build a higher-level concept of what RenderWindow::new() is supposed to do. As a bonus, it delineates a potentially helpful refactor; you can replace RenderWindow::new() with the (less Java-y) Window::render(). In essence, the sense that the function signature is too long is an indication that your API needs more building blocks.

I understand how folks coming from python naturally reach for the most 'convenient' syntax to express something, because that's the pythonic style. But there's a difference between syntax that is in line with a language's goals and what is more idiomatic to specifically you. Python is not averse to preferring some really arcane syntax; I feel comfortable writing my_list[:-1][::-1] now that I've been using it for the past few weeks, but when I take breaks from my Python projects and come back to them, I have to look up all the shorthands again. While this suits python's aims of being expressive, it comes at the explicit tradeoff of language consistency. Rust, on the other hand, has very different goals than python. In light of Rust's stated goals of scaleability, maintainability, and building a culture of exceptional documentation, optional/keyword args are not appropriate for the language.

emlun commented 5 years ago

@ThomasKagan You say:

In light of Rust's stated goals of scaleability, maintainability, and building a culture of exceptional documentation, [...].

...but that is precisely the reasons why Rust should have named and default arguments. With only positional arguments, your functions are difficult to maintain, because you will inevitably need to add a new parameter somewhere, which breaks all call sites. The alternative is, of course, to define a new function that also takes the new parameter, but now you have two functions that do almost the same thing. If you then need to add even more optional parameters, there's a combinatorial explosion in the number of functions you need. This does not scale well (in terms of cognitive load) - it dilutes the documentation because you have to explain the difference between all those almost-the-same functions, and anyone reading the calling code needs to spend more effort to keep track of several variations of the same function.

Parameter structs and the builder pattern do solve the same problems, but that is, again, precisely the point - it's a coping mechanism. It definitely has it places, but for many simple cases it's like using dynamite to dig a ditch just because there weren't any shovels around. The fact that we can use design patterns to emulate missing language features is not always a good reason to accept those workarounds as desirable - on the contrary, it's a big reason why Java is so infamously verbose and clunky.

I should note that my main objection to verbose workarounds, like the builder pattern, is not that you have to write a lot of code. It's that you have to read a lot of code. Code that serves only to express implementation details behind the API. Boilerplate is not bad because you have to write it, it's bad because it's irrelevant noise that distracts from the relevant substance of the program.

I agree that functions with too many parameters is usually a code smell, but everything always depends on context, and too many parameters is mainly a problem when the arguments are all mandatory and not named. I think the benefits of named and default arguments far outweigh the risk that it might encourage someone to write functions that do too much.

tikue commented 5 years ago

@ThomasKagan

Then you could make default constructors that allow users to overwrite fields with Rust's struct update syntax.

This doesn't work with structs that have any private fields or which are marked #[non_exhaustive], so it's an incomplete solution at best.

oblitum commented 5 years ago

@ThomasKagan

It's a feature that belongs in the space of scripting languages such as python

It works well in Swift and has been in use for ages in Objective-C.

ghost commented 5 years ago

Haha, I figured my view probably wouldn't be the most popular in this crowd. Tbh sometimes it's fun and healthy to be the devil's advocate. I just ask the folks who gave a thumbs down reaction to my post clarify what exactly they didn't like about it, so I can see their perspective. I'd say I side most with @crumblingstatue, once he referenced anonymous structs (#2584)

I don't consider user-facing, public structs 'bloat' if they appropriately model your api's data. I should mention that I would be considerably less averse to, particularly, named default args (because this thread seems to discuss a few different features all as one), for the maintainability reason @emlun mentioned—especially if you're required to use the keyword at call site. I see how it could be helpful in Rust's case for accomodating/updating legacy code. In the case of RenderWindow, I would, personally, prefer my Window suggestion, but in theory if you were truly very really sure that a use case needs just a shovel, then fine. But even too many named arguments is a code smell, and for that reason I was hesitant about adding too much nuance to my previous post.

IMO, reading more code isn't worse than reading less code that is less clear—like my python example, and don't get me started on javascript. It's about being able to find the parts that are useful to you right now, and understanding them.

@tikue I don't see how this makes proper data modelling for function args infeasible. But I am open to more comment. @oblitum Could you delineate how those cases apply to rust as well? Why are those features in Swift and Objective-C, and how do those reasons align with Rust's goals?

oblitum commented 5 years ago

@ThomasKagan

@oblitum Could you delineate how those cases apply to rust as well? Why are those features in Swift and Objective-C, and how do those reasons align with Rust's goals?

Read this comment and this.

It would be great to have this but my actual view is that the boat has sailed for Rust, this should have been in upfront, I think now it's late to have this.

In programming, it's really a sad mistake that the mathematical Euler function notation has become the standard, it's not self documenting, its place is math, not programming. Named parameters and return should have been the norm for programming.

ghost commented 5 years ago

Haha, yeah that's fair. I would like it if all arguments are required to be named, as opposed to the python interpretation of keyword arguments, where they indicate something is optional and the keyword itself is optional.

david-mcgillicuddy-moixa commented 5 years ago

I personally see named args and optional args as two different issues. Named args, as previously mentioned, help eliminate entire classes of bugs involving positional args. As soon as you have multiple arguments with the same type, then the type system cannot help you any more.

I agree with the opinion that special Arg structs are one of the current best ways to solve this, but involve both boilerplate and code bloat, so I made a proc macro that generates Arg structs for methods, and then a macro that inserts the struct at the callsite:

#[named_args]
fn foo(a: i32, b: u32, c: String) {
    println!("a: {}", a);
    println!("b: {}", b);
    println!("c: {}", c);
}

pub fn main() {
    named!(foo(a: -4, b: 5, c: "n".to_string()));
}

Since this as implemented is just a syntax switch (what you type inside of the arg brackets gets transplanted into a foo_Args { ... } struct expression) you keep all of the support that struct creation expressions have. You also cannot mix the two different forms inside a method call - you must use either named or positional syntax for all arguments. I suspect this also unlocks ..foo_Args::default() syntax but I've not actually tried that.

Much like Future vs await, it seems to me that named args are a lot less controversial and can be added first. Furthermore, they IMO add real value just on their own and then we can have a different conversation about optional/variadic arguments.

E: looking at it again, I restricted the macro to just Idents, so the .. syntax is not supported in my little test.

Additionally, I do not see this as a subset of anonymous named structs, any more than positional function arguments are a subset of positional tuples (i.e., they're not). It would be ideal if the anonymous named structs had the same or similar syntax to named function arguments but they are different proposals with different goals.

david-mcgillicuddy-moixa commented 5 years ago

In terms of syntax for named (not optional) arguments, let's look at the anonymous named struct RFC: https://github.com/Centril/rfcs/blob/rfc/structural-records/text/0000-structural-records.md

  Named version invocation Positional version invocation
Struct Yes, T { a: foo, b: bar) Yes, T(foo, bar)
Anonymous Type (value) No, Structural Record RFC (possibly { a: foo, b: bar}) Yes, (foo, bar)
Function Call No, this issue Yes, f(foo, bar)

Open question 1: should there be a difference between functions with named arguments vs positional arguments? I.E. should named arguments be a semantic difference at the function declaration or just a syntactic one at the call site? Certainly there is a semantic difference between named structs vs positional structs, and probably between named anonymous types and positional anonymous types.

If not, then given that function declarations already have to name their arguments, one possibility is that named argument calls can just reuse those. Then, named arguments would be syntactic sugar over a call to the original function call with the compiler mapping your named arguments into the right position. The downside to this is we run into the issues with the public API, see the discussions about a potential fn foo(pub x: i32) {} earlier.

Open question 2: how should this interact with arguments named _?

Open question 3: how should this interact with the Fn traits, since the names are lost? Should they just be unable to be called with named arguments?

EDIT: the answer to all 3 questions is "it should not", since as described in tanriol's comment below the named arguments cannot be inferred in general from all positional function definitions, and there instead does indeed need to be a separate opt-in syntax. One thing I don't like about the pub syntax is that it's opt-in per field instead of opting in for the function.

tanriol commented 5 years ago

...given that function declarations already have to name their arguments...

The usual reminder: functions do not have to name there arguments, they provide irrefutable patterns for them. Given a function

fn process_pair( (id, name): (u32, String) ) { unimplemented!() }

that can be written in Rust today, there's no single obvious way named arguments interact with it.

david-mcgillicuddy-moixa commented 5 years ago

It seems to me then, that for such functions that don't name all their args, there's two possible approaches: 1) if we allow functions where some arguments are named and some are positional (I personally am against this) then only some of their arguments can be made callable by name, or 2) Those functions can't be named into name-callable functions and you'd get a compiler error if you tried. Something along the lines of:


The function foo  has non-named arguments and can't be made into a named argument function.
    #[named]
    fn foo(_: i32)
           ^----- this argument does not have a name
    Suggested fix: give the function argument a name like: fn foo(_a: i32)
tanriol commented 5 years ago

By the way, this is an interesting question. The leading underscore at the moment is an internal marker for "unused" due to the unused lints skipping variables with leading underscore. However, being able to call the function by argument names freezes them for API stability, which means that adding or removing the leading underscore becomes a breaking change too.

The suggestion above highlights this in an unpleasant way – it suggests that you provide a parameter name explicitly telling the user this parameter is (at least initially) unused. This information is going to be both outdated and provocative ("why do I have to specify this if it's not used anyway?!").

Basically this would deprecate the convention of "underscore-prefix the names of unused parameters" with all the associated churn. The other technically possible way out would be to special-case and ignore leading underscore for the purposes of parameter naming, but that's something I would be strongly against.

oblitum commented 5 years ago

...given that function declarations already have to name their arguments...

The usual reminder: functions do not have to name there arguments, they provide irrefutable patterns for them. Given a function

fn process_pair( (id, name): (u32, String) ) { unimplemented!() }

that can be written in Rust today, there's no single obvious way named arguments interact with it.

Something like this?

fn process_pair(pair (id, name): (u32, String)) { unimplemented!() }
process_pair(pair: (42, "something"));

Which follows Swift, that employs spaces to separate argument labels in the formal parameter list. I don't see any conflict with irrefutable patterns.

aledomu commented 4 years ago

I'll go into detail on each one:

What I'd really like to see is functions with multiple signatures that have the same types but different destructuring patterns, so for those cases where you currently write a match block that covers the whole function depending on some arguments, you don't use that block.

oblitum commented 4 years ago

It'd be a nice addition but it doesn't interact well with tuple destructuring.

🤔, the previous comments were just asking why is that and provided examples demonstrating that doesn't look like a real issue.

Actually, I think functions with many parameters is in most cases an anti-pattern and I favor creating types liberally to give your code context through the type system and write "small" functions (self-documenting code FTW).

https://github.com/rust-lang/rfcs/issues/323#issuecomment-508955630

oblitum commented 4 years ago

FWIW, Gleam, a statically typed language for the Erlang VM, written in Rust, follows Swift at providing labeled arguments:

https://gleam.run/tour/functions.html#labelled-arguments

aledomu commented 4 years ago

🤔, the previous comments were just asking why is that and provided examples demonstrating that doesn't look like a real issue.

I read that. For consistency that would require to bring the whole argument label/parameter name thing, which I'm not sure I like. But if it's backwards-compatible and optional, I'm not totally opposed to it, just not interested enough.

FWIW, Gleam, a statically typed language for the Erlang VM, written in Rust, follows Swift at providing labeled arguments

So...?

oblitum commented 4 years ago

So...?

No argument, hence FWIW.