[Pre-RFC] named arguments

Thanks for the reply!

Yes, I wasn't aware of this at the moment I posted. Now I am just waiting to see the different alternative syntaxes that get proposed before updating the text.

An arrow-like symbol makes me subconsciously think there is some "flow" happening. Like in functions where you have in -> out or match statements where you have if this => do this. In this case there is not really a "flow", and if you see assignment as a flow it is going in the opposite direction, which makes this less intuitive. It's not a blocker but something to take into account when deciding.

You mean you are not sure about named-only parameters (proposed by @matklad) or about the fact that the user can choose either positional or named if an argument is marked as named in the declaration?

2 Likes

What about "reversing" the flow?

fn foo(pub x: i32 {}

foo(x <- 5);

EDIT: Stupid not having had coffee before posting. Am now caffeinated, and changed to a suggestion w/o a parsing ambiguity that's obvious to me.

Looks suspiciously like a less or equal expression :wink:

2 Likes

Indeed.

This is currently the syntax for a placement-in expression.

1 Like

So it is. I’ll just go find a corner…

In other news, Rust is completely sold out of new sigils.

5 Likes

I mean that I am not sure about having the ability to have fns that can be called either by name or positionally, which then makes it necessary to have some additional way to say if you want only positional.

Maybe I'm misunderstanding things, but isn't this feature gated, i.e. not stable yet? IMO would be a shame if struct initializers and named parameters had different syntax.

EDIT: Of course this wasn't intended as a reply to myself...

No. In this proposal, arguments are positional-only by default.

fn foo(a: u32, b: u32) {}

Can only be called with positional arguments. Same behavior as current behavior.

fn foo(pub a: u32, pub b: u32) {}

In this example, arguments have been "promoted" to named arguments and can be called either by positional form or by named form:

foo(1, 2)       // Positional form
foo(a: 1, b: 2) // Named form
foo(b: 2, a: 1) // Order doesn't matter
foo(1, b: 2)    // Positional and named arguments can be mixed under certain conditions

Doing it like this has a couple of advantages:

  • There is no change in behavior for code that currently exists.
  • Named arguments are opt-in. You won't need to bump major versions if you change the name of your arguments unless you explicitly marked them public
  • You can "promote" positional-only arguments to named arguments without bumping major versions, because you can still call the function with positional arguments. Users can then progressively update their code to use the named version (or not)
1 Like

All examples given involve literals. What about: let z0 = 100.0; let v0 = 0.0; let g = 9.81; free_fall(z0, v0, g); As an alternative?

Just a note for why we shouldn’t use ::

fn print<T: std::fmt::Debug>(t: T) {
  println!("{:?}", t);
}

fn main() {
  print(&0: *const i32);
}
1 Like

You can certainly bind local names, but they have no enforced correlation to the function you’re calling. So it may help you understand your own intention, but it won’t help if you passed the arguments in the wrong order, for instance.

I really like the proposal. I’m not sure whether to only allow positional or named in a function call as mentioned by a few people as it wouldn’t be a transparent upgrade for users but I wouldn’t mind either way, I tend to use mostly kwargs in python as it allows me to not have to go the function definition.

In terms of the syntax, I agree that using the same as the structs values would be nice. I wasn’t aware of the type ascription RFC but I believe that kwargs (coupled with default params) is vital to allow nice APIs and should have priority in terms of syntax over type ascription (and I personally find the is keyword suggested as alternative in the rfc to be cleaner).

The := syntax is also kind of ok but would be a new syntax

1 Like

Another interesting point of precedent is Swift. One particular point of interest is that they decouple external names from internal names, permitting more “fluent”-reading APIs. For example:

func increase(by increment: Int)
increase(by: 3)

This suggests an easy opt-in for Rust, since adding an external name is a requirement. Swift also mandates order and incorporates the argument name into the type (this is another point not necessarily addressed in this RFC: what happens with e.g., fn() types and the Fn traits).

14 Likes

I’m not a fan of this RFC. It seems like it adds yet another thing beginners need to know to understand function signatures, and it doesn’t add anything over, say, defining and passing in a struct with the parameter values.

For example, we can already do this:

struct MyArgs<'s> {
    x: i32,
    y: i32,
    width: i32,
    height: i32,
    title: &'s str,
}

fn use_my_args<'s>(args: MyArgs<'s>) { ... }

fn main() {
    use_my_args(MyArgs {
        title: "My Title",
        x: 5,
        y: 10,
        width: 10,
        height: 10
    });
}

In fact, there are many APIs that already use this style. Why add another way to do the same thing?

Also note that though the RFC claims naming arguments would mean people don’t need to memorize the order, it would still mean that people need to A) memorize which functions take named arguments and B) memorize the names of the arguments. So I’m not sure that it saves us much there.

I’m not saying I’m against named arguments, but why are we working on them separately from default arguments and optional arguments, which are the things that IMO make them really useful (note that I’m not necessarily a fan of default and optional arguments in Rust, but that’s a whole other discussion)? Also, while I recognize that suddenly enabling named arguments could cause backwards compatibility concerns for libraries that didn’t expect the names of their arguments to become part of the public API, I really don’t like needing to “turn it on” with a special keyword. Especially one that needs to be repeated for each argument (surely once would be enough?). I’m not sure that I ever would use this if that was the case.

5 Likes

The issue with the struct approach is that you need one struct for each function, which is pretty terrible from a usability/API point of view.

I agree with eventually not needing the pub keyword in function definition though, it would be nice to have it working by default.

1 Like

Yeah I agree that the struct approach is not ideal, but it is a viable alternative, I just don’t think this RFC adds enough to justify implementing it.

Also note that most of the time one really shouldn’t need to use named arguments or pass in a struct configured with the different options. IMO it’s really only useful if A) you have a lot of different ways in which something can be configured or B) you have a lot of arguments that all look similar (like x, y, width, and height). Otherwise it doesn’t really seem to add much. So, I think saying that you’d need a struct for each function is an exaggeration, because IMO using named arguments every time you call a function actually makes code harder to read and write anyway.

Interesting, though this could lead to a lot of repetition in function declarations when you have the same public name as internal name.

Could you explain your concerns?

Creating a struct for the majority of functions just to be able to name your parameters is a lot of boilerplate as you can see in your own example. Named arguments are more ergonomic and probably a lot easier to get for beginners.

It is not exactly the same thing. It's another tool that you can use for specific situations. I'm not saying named arguments are the solution for every situation, builder patterns, struct arguments, etc. should still be used when more practical. But like there exists multiple loop constructs (+ iterators) there can be multiple tools to make APIs more ergonomic.

Because default arguments spawn a lot of additional discussion on technical problems, debate on best practices, etc. etc. Those discussions are not all relevant to named arguments and this proposal is a way to get things moving in the right a direction.

Of course, I mentioned in the proposal that we should think ahead to limit the restrictions we set for the possible future addition of default arguments.

I would not want my argument names to be part of the API by default. Renaming an argument would then automatically require a major version bump. This means you have to carefully think about the best possible argument names in addition to the function name when you introduce it. And for me, personally, my argument names tend to be relatively mediocre until I find better names along the way.

As for the repetition, I agree that having pub, pub, pub repeated multiple times in the function declaration is not exactly pretty. But only giving a binary choice of either no arguments are named or all arguments are named seems a bit limiting.

Obviously not all arguments need to be named. Take the example I gave with

http.get("https://www.rust-lang.org/en-US/", timeout: None);

It's pretty obvious that you want to pass a url as first argument. But having a trailing None could mean pretty much anything. Creating a struct just to be able to name that one argument is IMHO pretty silly.

5 Likes

I will say that named arguments makes refactoring much less painful (adding and removing parameters to APIs has less room for error), and provides useful context-clues when reading code that uses the API.

One other thing not mentioned explicitly here is that the “no unnamed arguments after named arguments” matches the behavior chosen in Python, and follows the “Principle of least astonishment”, which helps with usability in my opinion.

This also reduces the need to use a builder pattern (which is verbose for API writers, though less so for API users). See Conrod for an example here.

3 Likes

What is the trait signature of fn foo(bar: u32, pub baz: u16, pub quux: u16)?

Is it Fn(u32, u16, u8)? Is it Fn(u32, baz: u16, quux: u8)?

If the latter, how does this translate to the "unsugared" traits? Does this mean introducing named type parameters as well?

3 Likes