[Pre-RFC] Named .arguments

It might be better to think of the argument labels as part of the name of the function. It's harder to come up with a composite name syntax with .name = value syntax as compared to 'name = value syntax, though, since foo.name looks like a field access rather than a composite name.

let f.b:   fn(i32) -> i32 = foo.bar;   // looks like field access
let f'b:   fn(i32) -> i32 = foo'bar;   // looks like lifetimes
let f.=b:  fn(i33) -> i33 = foo.=bar;  // just looks odd
let f#b:   fn(i32) -> i32 = foo#bar;   // people might expect this to be an attribute syntax?
let f(.b): fn(i32) -> i32 = foo(.bar); // looks like pattern matching

What would that achive? I'm not sure what the motivation for this is.

1 Like

It would allow callable values to have argument labels as part of their name rather than as part of their type. Putting the information in the type causes issues as far as compatibility between function types with argument labels and function types without.

If I can write

fn foo(.bar x: i32) -> i33 { x }

let f#b: fn(i32) -> i32 = foo#bar;
let x = f(.b=5);

the argument labels aren't completely lost once you're using some sort of function value.

This might be useful in some cases, but I believe they're much less common than the cases covered by the RFC (calling a function directly, not via a function pointer or closure).

Besides, if this is used in generic code, it might be detrimental to composability. For example:

fn foo(f#lhs#rhs: impl Fn(char, u32) -> bool) {}

// could be called as
foo(|.lhs c, .rhs radix| c.is_digit(radix));
// but this doesn't work, because the argument names don't match:
foo(char::is_digit);

Most importantly, I don't think it's worth the additional complexity in the syntax and the type system.

1 Like

Sorry, I think you're misunderstanding me. The idea is that the argument labels are part of the identifier. They have no implications for type checking.

So when passing it to a function or assigning it to a new variable, the argument labels are allowed to change?

Yes, just like the name is allowed to change. If I write

fn double(x: i32) -> i32 { 2 * x }
...
let triple: fn(i32) -> i32 = double;

there's no check that prevents me from giving the variable a misleading name which is different from the name of the original function. The names document the call-site, they don't enforce a contract. If you want a contract enforced at the type level, you need an explicit trait.

1 Like

Coming by with a quick nit: the Pre-RFC text says "[in] Python every argument can be used as a named or positional argument"

This is not quite true. In python, named arguments are called "keyword arguments". Python allows the function to declare an argument as positional-or-keyword (the default), positional-only, or keyword-only:

def f(positional_only, /, positional_or_keyword, *, keyword_only):
     pass

#can be called as:
f(1, 2, keyword_only=3)
f(1, positional_or_keyword=2, keyword_only=3)

#cannot be called as:
f(1, 2, 3)
f(positional_only=1, positional_or_keyword=2, keyword_only=3)

This syntax for keyword-only arguments were introduced in python 3:

And the syntax for positional arguments in python 3.8:

Although it was actually possible for functions to manually implement positional-only and keyword-only arguments before those features by parsing *items and **kwargs manually, and several standard library functions had positional-only or keyword-only arguments.

I bring this up not just to nitpick (although I suppose it is partially that). I think the write-ups in those PEPs provide useful prior art for the use of requiring named arguments to be named at the callsite.

5 Likes

Thanks for pointing this out!

I'll note that the rationale developed in PEP 3102 doesn't really apply to Rust as it is (at least until a serious variadic generics proposal arrives):

This is not always desirable. One can easily envision a function which takes a variable number of arguments, but also takes one or more 'options' in the form of keyword arguments. Currently, the only way to do this is to define both a varargs argument, and a 'keywords' argument (**kwargs), and then manually extract the desired keywords from the dictionary.

1 Like

What if Rust made named arguments strictly a caller-side syntax sugar? Any function could be called as either select_until(value) or select_until(.until = value). Correct argument order must still be used.

Pros:

  • It would add an extra layer of clarity + compile-time checking if the programmer is dealing with a poorly written API.
  • API developers will not be tempted to use kwargs over types, eg Window::Title and Window::Size.
  • It will be retroactively available to all functions in the std library.

Cons:

  • Will make a Rust ABI more difficult. Functions now need share their argument names.
  • Will be backwards incompatible with or will have to special case function pointers as their argument names are currently optional.

Non Issues:

  • the _ argument name. Argument order is not allowed to change so calling a function like foo(._ = bar, ._ = baz) should not be an issue.

_ is not really a name but a pattern. In case you forgot or have not really seen it yet, in Rust you can pattern match on function arguments as long as the pattern is irrefutable (i.e. same condition you need to use pattern matching in a normal let). For example you can do something like this:

type Point = (f64,f64);

fn foo((x,y): Point) {
    println!("x is {} and y is {}", x, y);
}

(playground)


So, back to the main topic then: I guess the approach of using syntax like ._ does not scale that nicely to a pattern like (x, y).

2 Likes

If you're suggesting that fn example(argument: usize) would be callable as example(.argument = 5), that is not considered an available option. This makes argument name/patterns a stable part of the API when they aren't currently, and is a huge semver hazard, as changing argument patterns is perfectly allowed today.

Any solution, even if it is just optional argument names at the call site, still needs to be opt-in on a function-by-function basis, so that argument names aren't part of public API by default (because they aren't currently).

10 Likes

I have now rewritten some parts of the RFC and added more details, but before publishing the RFC on GitHub I'd like to hear more opinions if named arguments should be enforced at the call site.

The main benefit of lifting the restriction is that named arguments can be added to existing functions backwards compatibly. Also, when specifying the argument name does not improve readability (e.g. foo(.arg = arg, .data = self.data, .blah = blah()) it can just be omitted. A compiler warning or clippy lint can be added to warn when a named argument should be used but is omitted.

I think they should *be in the initial version. This restriction can be lifted in the future but the reverse isn't true (you can't add this restriction if it wasn't there since the beginning).

  • Maybe having the same mechanism to not have to repeat the field of a struct is enough (ie: calling fn get_position(.lat: f32, .lon: f32) with two variable lat and lon could be abbreviated with fn(lat, lon)).
  • Maybe it should be the responsibility of the author of the function to have optional keyword (ie: declaring a function where some parameters can be called using either positional or keyword argument as the caller prefer).
  • Maybe all keyword arguments should be usable using the positional form.

I don't think that any of those 3 questions are easy to answer, and make a strong consensus, so I think they should be left for later.

1 Like

I think that this need to be detailed in the RFC.

As a really conservative first version, it could be said that function with named arguments cannot be used in such situation. The reasoning is simple: it's always possible to create a function with positional-only argument as glue, and the delay all those complicated question to a later improvement of named arguments.

1 Like

Note for future bicksheeding: I think that the shorthand version should be foo(argument) and not foo(.argument). This doesn't need to be discussed right know since I don't think (even if I would like) that this RFC needs to include any kind of shorthand.

Hard disagree on both counts. You can add a restriction with an edition change or in a project's linter settings, and having named arguments be mutually incompatible with positional arguments creates ecosystem fragmentation (libraries have to add a major semver version to include named arguments) that can calcify even after the restriction is lifted.

I think this decision should be taken under the assumption that it will be final, one way or another; there's no reason to put it off until a hypothetical time where we'd have more information. This is essentially a philosophical choice, and we already have all the information we need to inform that choice.

(unlike, say, async, where the design rust picked is pretty novel and doesn't have a lot of precedent in mainstream languages, so it makes sense to wait to see what users do with it before committing).

4 Likes

The exact sense in which named arguments are "mandatory" or not is also an extremely huge factor how well-motivated they are.

Remember, it's still not a given that "we" (in the broadest possible community consensus sense) want any form of named arguments at all. Many of the arguments in their favor only apply to certain forms of them, which is a big part of why there's so many differing opinions on how compelling those arguments are. As just one example: if you ever want named args in the std library then they must be optional, but then you risk losing a lot of the force behind the "make code more readable" arguments, especially if (as I suspect we do) we still want some form of structural records/anon records and those end up providing a "more mandatory" alternative.

8 Likes

That's absolutely right, and I didn't considered this when giving my reasoning.

Why? I think that there are different possible outcome:

  • Do nothing,
  • Mandatory named arguments, not used in std => named arguments may not feel "first class"
  • Mandatory named arguments, used in std only for new items => different styles in std, named arguments may not feel "first class"
  • Mandatory named arguments, used for everything where named arguments make sense => duplicates API for lots of things, may need to deprecate a lot of std (most the current functions basically since they use positional arguments), may need a lot of work to convert everything
  • Optionally named arguments - opt-in (ie each arguments can be used as positional or named arguments, at the discretion of the API author) => no change in std, named arguments in std don't feels really useful, but future API can take advantage of it.
  • All named arguments can be used positionally => no change in std, but I think that we miss the raison d'être of named arguments.

I think that nothing is optimal. I would personally prefer opt-in optionally named arguments. But I would highly prefer to have mandatory named arguments without any change in std, and to be able to use them in non-std crates, than not having them at all.

5 Likes