Pre-RFC: Named arguments

Function calls sometimes have one (or more) very long argument, and in that case it (IMO) usually improves readability to put the long one last. As an API designer, I try to order my functions' parameters to gain that benefit, but sometimes the best order by that metric varies by call site.

2 Likes

Sounds like a code smell. If the argument is too long it ought to be extracted to a variable with a meaningful name (which I sincerely doubt would be exceptionally long)

1 Like

We can have poor man's structural records tomorrow. As a library. I brought up this technique in URLO, thread Named arguments patchwork, which was not entirely serious.

#![allow(incomplete_features)]
#![feature(adt_const_params)]

struct Arg<T, const NAME: &'static str>(T);

macro_rules! record {
    ($($arg:ident: $typ:ty),*) => {
        ($(Arg<$typ, {stringify!($arg)}>),*)
    };
    ($($arg:ident),*) => {
        ($(Arg::<_, {stringify!($arg)}>($arg)),*)
    };
    ($($arg:ident = $value:expr),*) => {
        ($(Arg::<_, {stringify!($arg)}>($value)),*)
    }
}

fn from_polar(args: record!{radius: f64, angle: f64}) -> (f64, f64) {
    let record!{radius, angle} = args;
    (radius*angle.cos(), radius*angle.sin())
}

fn main() {
    let (x, y) = from_polar(record!{radius = 1.0, angle = 0.0});
    println!("({}, {})", x, y);
}
5 Likes

Edited so as to adhere to community guidelines.

Firstly, I agree that having inferred struct literals would be a more robust solution.

But considering that:

  1. This is OP's first pre-RFC.

  2. The "typo" you refer to is not a typo.

  3. You can misspell struct field names just as easily, and then you can't fix the interface without violating backwards-compatibility either.

  4. You argue that using meaningful types is "correct design", yet standard library clearly doesn't use these consistently (using usize for indices being highly non-semantic, even limiting in that a special type could handle indexing from the end). I therefore feel like the main motivation isn't "correct design", but inertia.

  5. You argue that languages where named params make sense are all dynamically typed, clearly not considering Swift, C#, or Scala.

It's you being rude, not the OP. That you feel insulted by this post being duplicate of older ones - and complaining about it instead of eg. providing the links to the older posts - is something I don't get.

For information, some other posts about named arguments:

3 Likes

Moderator note: Please refrain from personal attacks. You can take issue with someone's behavior without making unkind remarks about their personality.

6 Likes

I really would just like to make a bit of a meta remark at just how refreshing it is to see such a well-written RFC written by somebody who has clearly read all of the past discussion, and who has new ideas to bring to the table on a topic that has been mostly running around in circles for years.

The proposal here is extremely cohesive and has a well-defined scope. I can see how each design decision and limitation (e.g. lack of reordering, forced usage at callsites) makes sense in the context of the rest of the design. At first I was unsure why overloading is a part of it, but as I read through it became clear that this was necessary to allow backwards-compatible adoption by existing APIs. You've certainly found a local maximum in the design space, and I don't think any small part of it can be easily tweaked without changing a bunch of the rest.

There are not any concrete criticisms I have been able to come up with which are not already at least acknowledged by the proposal. (For instance, the fact that callers may be forced to write name: name.)


So, about the proposal itself? Well, considering the fairly limited scope of the problems it is designed to solve (especially in comparison to other named argument proposals):

  • Providing library authors with an easier (for the author and for the user) alternative to newtypes/builders for a function which faces the problem of, "it can be unclear what this bool parameter is at callsites."
  • Letting a library author provide ::new(bar: ...) and ::new(foo: ...) rather than ::new_bar and ::new_foo

I feel the benefits are unlikely to outweigh the costs in additions to the language:

  • Addition of parameter names to the type system (in particular to fn types and Fn traits).
  • Argument names playing a role in function name resolution.
  • Syntax for:
    • defining named parameters
    • using named arguments
    • qualifying overloaded functions

and thus, I generally wouldn't expect to see it accepted in its current state.

The idea of "named arguments" brings with it a lot of baggage and expectations from other languages, and I get the feeling that many users would be excited to hear "rust is getting named arguments" only to then be frustrated and confused by the restrictions here. "Overloading via named parameters" is perhaps a more direct description of the idea.

22 Likes

This proposal really isn't "Overloading" in the sense that that word normally implies. It isn't overloaded based on the types of the parameters; rather, the function name is effectively the name declared for the function plus the names of the named parameters.

I think if every place where this RFC mentions "overloading" it was changed to "function name resolution" and emphasized the fact that the function name really includes the named parameter names and there really isn't any "Overloading" of the same function name, then I think it would be less controversial. I really think a lot of people are getting hung up on "Overloading" when it is really a non-issue.

4 Likes

Whether you call it "overloading" or not, it does make function name resolution a lot more complicated (e.g., use module::foo can now bring multiple functions in scope), it seems like a challenge to support nicely in rustdoc, and it means that creating a function pointer by just writing module::foo no longer works.

Right now, the syntax and semantics for f(args) is very compositional in the sense that f is an expression that evaluates to a function type, and that's all there is to it. This proposal breaks that property, and means that args needs to be taken into account to figure out what f will resolve to. It makes no difference to me whether it is the types or names in args that affect this. I think it is fair to call all of that "overloading" and like many others here I am concerned that this is a misfeature. The argument part of the function call syntax has no place affecting the function part of it in my view.

Furthermore, isn't it the case that argument-name-based-overloading/resolution could be added later? So, in the interest of having a smaller RFC with fewer controversial parts in it, and adding fewer new complexities to the language at once (I mentioned some of the challenges above), I think this RFC would fare a lot better without the overloading part.

21 Likes

That's a big part of what overloading is. The objection isn't to the terminology, it's to the concept.

7 Likes

What is objectionable about the concept:

parse(from: some_str, into: some_structure)?;
parse(from: some_str, into: some_structure, with_filter: some_filter)?;

vs.

parse_from_into(some_str,some_structure)?;
parse_from_into_with_filter(some_str,some_structure,some_filter)?;

because, to my reading, that is what effectively this RFC proposes. I just don't see how that is anything like overloading based on types. It is clear that the first option reads better and is clearer in intent. I can't think of any reasonable way this can be found objectionable other than a reflexive abhorrence of "OVERLOADING" that isn't justified.

4 Likes

The inability to just write the name of a function to reference that function, whether in code, documentation, or other communication. The need for a new syntax to reference specific overloads of a function based on their parameter names. The need to support and use that syntax in any new places that want to reference function names. The added complexity for future use and stabilization of function trait implementations.

Also, there's no need to use a name like parse_from_into and spell out all the common arguments in the name; that could just be parse.

See above. And in general, please don't assume that people who don't share your position have a "reflexive abhorrence ... that isn't justified". That kind of dismissal doesn't invite answers or lead to understanding; it just characterizes others as incomprehensible eldritch roadblocks to your unassailable position.

20 Likes

What about if it worked as follows:

fn parse( pub from: &str, pub into: MyStructure );

defined the function named, "parse$from$into" (or "parse__from_into" if you prefer). Then, at the call site:

parse(from: s, into: my_structure);

Is resolved unambiguously as "parse$from$into". Also, if you want a function reference or you want to "use" the function name, it is simply "parse$from$into". Absolutely no new syntax required and 100% unambiguous.

Does that resolve the objection?

EDIT:

This also resolves the issue for C-FFI. In that case, if you have more than 1 function with the same root name but differing parameter names, you would simply have 2 different symbols exported and callable from C without any ambiguity. So, no special rules are required around C-FFI. Also, this would make unique symbols in the debug format as well as the compiled code for purposes of linking. In effect, this is just another way of declaring the name of the function. You now have the root of the function name, appended with the "pub" parameter names.

Does this start to sound more palatable?

Why does this need argument-based overloading at all? Why not use different function names? That would give unique unambiguous names and symbols, with no special syntax.

That's still new syntax, it's just different syntax.

6 Likes

I'm not understanding why you think that is new syntax. Can't you already use "$" in symbol names (am I mis-remembering)? Oops. You can't currently use $ in symbol names. Ok, then the function would be named, "parse__from_into". That has no new syntax. You can even call the function using that full name without using the parameter names. So, you can call this function like this:

parse(from: ..., into: ...);

or this:

parse__from_into( ..., ... );

You can import/use the function like this:

use somemod::parse__from_into;

You can get a function pointer like this:

let fnptr = parse__from_into;

None of this requires new syntax (except at the definition site and at the calling site when specifying the parameter names). However, the function name can be de-sugared using the root name and the parameter names 100% unambiguously and require no further special handling by the compiler.

No?

EDIT: The "parse( pub from: &str, pub into: &mut MyStruct )" is a very poor example, but it is simple enough to illustrate the point.

Couldn't this also be valid? You could just consider the number of parameters as part of the function name.

parse_from_into(some_str,some_structure)?;
parse_from_into(some_str,some_structure,some_filter)?;

My question is: why is this bound to named parameters? Why is this essential to them?

2 Likes

I'd consider this a straw-man. It doesn't really have anything to do with the proposal.

Overloading also doesn't have anything to do with named arguments.

In general, unless there's some fundamental reason that two features need to go in simultaneously, I'd suggest separating them into separate proposals so they can be considered separately. The primary reason I'd suggest putting otherwise unrelated features into the same proposal is if you want to make an argument of the form "adding A and B together improves the language, but adding A without B makes the language worse; we shouldn't add A without B".

Do you think named arguments without overloading would be worse than no named arguments at all? If not, is there some reason to not let named arguments be considered on its own, separate from overloading

7 Likes

I think this is an excellent demonstration of why it's essential to have a scope discussion first, and then say why the chosen mechanism is better than other alternatives that could also meet those requirements :+1:

I don't think so, so I'll elaborate it out a bit.

Rust doesn't have arity-based overloading today. But at any point in the past 7+ years Rust could have done an analogous "no new syntax" change to say that fn foo(_: i32) would actually be named foo__1, and thus if you also have fn foo(_: i32, _: u32) that'd be fine, because it would actually be foo__2, and not conflict. Then you could have use whatever::foo; import both, but have use whatever::foo__1 if you only wanted one of them.

That's actually much easier than doing it for named arguments, since it doesn't need the annotations inside the arguments. But yet while arity overloading has come up occasionally over the years, it's never really gotten close, in my impression, to the kind of groundswell needed to make it actually happen as a language change, despite being a feature that's incredibly common in other languages. And is something that would be handy for compatible upgrades to existing things -- all the _in methods that also pass allocators, for example.

So what is it about overloading with named arguments that makes it substantially more worth doing than overloading on arity? Or are they both about the same value, and something has been missed about the arity version that says it's actually important to do?

(And I think the emphasis on "no new syntax" is a bit overblown, since it opens up all the questions about ambiguity between fn parse__from_into(); -- which is legal today, albeit discouraged -- and a future fn parse(from: ..., into: ...);. If this is worth doing, I wouldn't mind at all a bit of extra syntax for it, especially if it can fit in already-reserved space like 3101-reserved_prefixes - The Rust RFC Book .)

2 Likes

Yes, I think not having overloading (which I don't really think it is overloading, but nevertheless) with named arguments is not worth the effort. I really think these things go hand in hand.

Without the "overloading" part of it you can't make "sentence-like API's" that start with the same verb. With it, you can. Being able to have a call site read like a sentence is incredibly useful for clarity of the code and makes understanding someone else's code much easier.

As I said above, it makes it possible to have API's that read like sentences at the call site and also have sentences that start with the same verb but contain differing additional clauses.