[Pre-RFC] named arguments

Hm, thats very subjective. The beauty of the ObjC Cocoa API is what got me hooked into programming in the first place. I wouldn't be here as an enthusiastic software engineer if it weren't for these APIs.

Indeed it is, and I canā€™t claim otherwise. But it is one very real reason for people like me to stay away from programming languages with that ā€œextremely long fn namesā€ trait. I guess I just donā€™t see the point of something like "[giveMeYourMoney: "all", andThenOnRobberyDone: some_action]; when something like rob_money(MoneyTransferType::All, some_action); does the job more succinctly while remaining clear in intent.

Add to that 2 facts:

  • Ultimately this problem can be seen as simply tying more-or-less random symbols (i.e. function and var names) to some kind of meaning (a fn body or var definition). Of course theyā€™re not completely random since the entire point is choosing symbols that evoke the corresponding semantics in peopleā€™s minds (a computer doesnā€™t care whether something is called a or my_descriptive_pony).
  • Humans have a limited working memory, even the best of us

Given the above, I reach the conclusion that shorter symbols are better, especially when juggling a number of them at any one time*. This matters because forgetting a symbol means having to look it up, an action which costs time. For one such symbol it usually doesnā€™t take more than a few minutes at most, but such quanta of ā€œlost timeā€ can quickly add up since itā€™s a systemic factor i.e. a forgetful person isnā€™t particularly likely to start remembering things better out of the blue.

But apparently it doesnā€™t work like that for everyone? In the interest of clarity and perhaps a better-informed end decision, would you mind attempting to explain how that process works for you?

*Compare this to how many numbers a person can at any one time remember reliably, I think remembering numbers in this way and remembering symbols works very similarly though not identically since numbers usually have no meaning attached for us, which influences our ability to remember them.

One benefit is that it allows generic code to call a function with named parameters. Presumably this still has all the same motivations about clarifying ambiguous argument order. Something like:

fn move<F>(&mut self, callback: F)
where F: Fn(lat: f64, lon: f64)
{
  for pos in self.update_positions() {
    callback(lat: pos.lat, lon: pos.lon);
  }
}

This might be the same as your point about closures, but I'm not sure.

1 Like

For the parsing ambiguity, it would probably be worth mentioning

as an option even if it would affect type ascription syntax. Though that particular comment didn't get any direct replies, there were a few mentions:

but none added much to that discussion aside from opinions.

Another thing mentioned multiple times was omitting the argument name when the name of a variable being passed in matches. This is different than just passing said variable as a positional argument. This would address this comment:

and was mentioned three separate times that I see:

1 Like

I think this would be a bad idea if we allow both positional and named form, because you would have no way to immediately know which of the two forms is being used. Simply renaming a variable could cause a subtle change from the named form to the positional form. Best case scenario, you get a compile error because the type don't match, worst case, you silently introduce a logic bug.

2 Likes

All three of the mentions I quoted used some syntax that made it explicit that it was a use of the named form which omitted the (redundant) name. Using the syntax originally proposed for named arguments, the name could be replaced with an underscore (example for demonstrative purposes only; actual syntax would depend on the rest of the named arguments syntax):

fn increment(amount: i32, pub by_amount: i32) -> i32 {
    return amount + by_amount;
}
let amount = 4;
let by_amount = 6;

println!("{}", increment(amount, _: by_amount));

Changing a variable name should give a clear compile error when it doesnā€™t match a named argument.

Ah yes! Sorry, I missed that. Personally, I feel that if you are going to omit the named arguments you should go with the positional form instead. In a language where you would be forced to use named arguments this feature would make a lot of sense. But if we allow to freely choose between named and positional, the only advantage I can think of would be to reorder parameters.

Depending on where things fall (re: Should named arguments be reorderable at call-site?), the benefit would either be a compile time check that you have the order correct or simply not having to worry about it. If they were just positional args, they could mistakenly be entered in the wrong order even with the correct names. Avoiding that seems to be one of the major motivations behind named arguments. Either way, those three people seemed to come to the same conclusion independently, which seems to merit a mention in the summary.

As small time rust user I would like to chip in. This feature seems like natural fit into the langauge for me. I would advocate for folowing form:

  • fn(name:val) syntax
  • supporting positional arguments in place of named ones.
  • correct order preserved by the compiler(can be changed later - backwards compatible & conservative solution
  • Positional argument cut-off : after named argument has been used, no more positional arguments can be used ( common, safe & elegant solution )
  • using pub or similar keyword to specify which arguments are named.

Explanation

Iā€™m in favor of fn(a:b) syntax because it follows already used conventions and ā€œfeelsā€ natural. a=b case is also very nice, but since it is also an expression with () return type, it would probably mean some special case handling.

Most frequent use of : is in structs. When declaring new struct, usage is name:type. When creating new instance of this struct, usage is name:value. This whole concept is really elegant, since : signifies concept of binding. When you are defining struct, you are binding the type to name, and when instantiaring, you are binding value to name. Similarly, when defining function, you use : to bind parameter type to parameter name. I think preserving this concept in function calls should be a priority.

Type ascriptions do not follow these conventions, since the form vale:type does not express binding, it is there simply to help the compiler with type inference. I think the is syntax would have been more fitting.

Making named arguments opt-in is a no-brainer. Backwards compatible & non-interfering solution.

Enforcing argument will probably be almost friction-less to programmer, prevents subtle bugs presented by graydon and can be changed later in backwards compatible manner.

No positional arguments after named one : ditto. Safe choice that can be extended later in backwards compatible manner if needed

I also think usge of pub to specify first named argument/which arguments are named is great idea, since by using named arguments, the argument names become part of the api.

Other

Default arguments: subject for another discussion, would fit right into propsed variant of named arguments with name:type = value.

Implementation difficulty of this should be minimal.

The only BIG problem is collision with type ascriptions, if type ascription syntax is set at using : then there are few solutions:

  • Disallow type ascription in arguments.
  • Parentheses

Anyways my 2Ā¢. Sorry for typos, written on mobile.

4 Likes

I am wondering the same as @Petrochenkov, and it seems that nobody else has even touched on it. He already asked most of the same questions I am about to, but I will extend them with code examples.

I have a kneejerk reaction of suspicion towards any proposal which talks about ā€œparameter names,ā€ because to remind everyone of the status quo in Rust: Parameters currently do not have any identifier associated with them, period!

Despite typical appearances, the names you see in the parameter list are NOT identifiers! They are destructuring patterns:

// equivalent signatures, different number of bindings
fn takes_a_tuple_1(tuple: (A,B)) -> C;
fn takes_a_tuple_2((a,b): (A,B)) -> C;

// equivalent signatures, different types for 'x'
fn takes_a_borrow_1( x: &i32); // x is &i32
fn takes_a_borrow_2(&x: &i32); // x is i32

// similar example
struct NewType(i32);
fn takes_a_newtype_1(x: NewType);          // x is NewType
fn takes_a_newtype_2(NewType(x): NewType); // x is i32

This opens up a world of questions:

// Can you do these? What names and types does a caller provide?
pub fn am_i_valid_1(pub (a,b): (A,B));
pub fn am_i_valid_2(pub &x: &i32);

// Or do we introduce some wacky extension to destructuring syntax to
// let us expose the names of the *bindings?*
// (this idea is silly because it would allow the named args
//  to have different types from their unnamed counterparts)
pub fn am_i_valid_3((pub a,): (A,)); // named arg is a: A
pub fn am_i_valid_4(&pub x: &i32);   // named arg is x: i32

// Or do we introduce a way to simultaneously name and destructure parameters?
pub fn am_i_valid_5(pub point@(x,y): (f64, f64));
    //   named arg is  point: (f64, f64)  (but not x and y)
    // local vars are  x: f64, y: f64     (but not point)
5 Likes

Considering that people want internal external names, I think your last alternative makes a lot of sense. I donā€™t have enough experience to discuss the technical implications however.

2 Likes

Pretend for a moment that Rust functions only ever take single arguments (besides self). Consider that the current positional syntax is equivalent to passing an unnamed tuple to your function, and is, in fact, sugar for exactly thatā€¦e.g. the function:

fn my_func(a:i32:b:u64,c:&str) {};

can be called as:

my_funct(1,2,"foo");

but this is really shorthand for:

let a=(1:i32,2:u64,"foo":&str);
my_func(a);

or, if done anonymously

my_func((1:i32,2:u64,"foo":&str));

So the current behavior is nothing more than parentheses elision for anonymous tuples

Unless Iā€™m missing something, this re-imagining of existing function arguments would then be fully backwards compatible

Given the RFC for anonymous structs, then, supporting them as arguments would be perfectly parallel

fn my_func({a:i32,b:u64,c:&str}) {}

could be called as:

my_funct({a:1,b:2,c:"foo"});

or if brace elision is allowed:

my_func(a:1,b:2,c:"foo");

or explicitly

let my_val = {a:1i32,b:2u64,c:"foo"};
my_func(my_val);

or if positional support is also allowed:

my_func(1,2,"foo");

This would unify fn arguments, tuples, and structs, and allow for any expansion of structs to enable default arguments to apply here as well.

Assuming there is not fatal flaw with the above, Iā€™d probably prefer that named parameters fns not automatically also support positional call syntax, but if desired could be #derived with an annotation

TL;DR by re-imaging all function arguments as unnamed tuples, unnamed structs can be perfectly ergonomic as named arguments.

6 Likes

Struct patterns already provide a way to simultaneously name and destructure pattern. And since parameters are also patterns, I think this would be the most natural way:

pub fn am_i_valid_6(pub point: (x: f64, y: f64));

I also really like @tupshinā€™s suggestion!

2 Likes

To unify the RFC with @tupshinā€™s suggestion: If the parameter is preceded by pub it can be interpreted like a struct field in a pattern, otherwise like a tuple element in a pattern.

1 Like

Iā€™m in favor of named arguments but I donā€™t like the idea of differentiating between functions with and without named arguments or that only some fields are named whilst others are positional.

My proposal is to either allow positional or named arguments at the call site, but not both. This would be analougus to structs (named) and tuples (positional). The former form would feel much like Smalltalk and the latter is what we use right now:

// Hello Smalltalk
inc(value: 2, by: 4);
inc(by: 4, value: 2);

// Forbidden: Either {} or () semantics.
inc(2, by:4);

// Old style
inc(2, 4);
// (2, 4) is transformed to { value: 2, by: 4 }

Further I liked the idea of being able to support a public and an internal name. Since : is already used, we could use -> in the argument list to specify aliases.

fn inc(
    // `value` is the same (public and internal)
    value: u32,
    // `by` is public API, `other` is used in function body
    by->other: u32
    ) -> u32 {
    value + other
}

We could than desugar this to something that works today:

struct IncArgs {
    value: u32,
    by: u32,
}

fn inc(IncArgs{value: value, by: other }: IncArgs) -> u32 {
    value + other
}

// calling
inc(IncArgs{value: 2, by: 4});

// inc(2, 3) will also be converted into named style calling

However with current rust this is impossible, since argument names are not part of the type:

trait Foo {
    fn foo(&self, u32);
    // a name can be used for documentation
    fn foo(&self, arg: u32);
}

fn take_foo(obj: &Foo) {
    // It is not possible to specify the name of the param here.
    obj.foo(42);
    // not possible with current model, because implementations can choose a name they want
    obj.foo(arg: 42);
}

The type of Foo.foo is Fn(&self, u32) and not Fn(&self, arg: u32). The same applies for closures.

Thus, adding named arguments in a way that feel natural (at least to me) would require major changes to the languages which would not be backward compatible.

Note that in my proposal above,

named parameters fns [should] not automatically also support positional call syntax, but if desired could be #derived with an annotation

I feel like this is the best of both worlds. You can consider that derive annotation are effectively generating a completely new function and signature. Nearly eliminates the burden for anybody that explicitly wants to support both forms in their library, but doesn't introduce accidental api breakage and confusion.

A couple of comments from an interested observer:

  1. Please, add at least some version of named args. Even if it has fixed argument order and forbids mixing with positional arguments. These restrictions can be lifted later if they are deemed unnecessary.

  2. I donā€™t like renaming of fields to get different internal names, purely because it adds more clutter to function definitions (which are often already severely overloaded).

1 Like

I was going to make an RFC for this as well, then got directed here. It would have looked like this one. Should I make my points here, or start another thread? This one seems to have just stopped, and never made it to RFC stage, so Iā€™m unsure as to the status.

Iā€™m glad it stopped, since it canā€™t really be discussed forever and continue to go nowhere. But fundamentally, thereā€™s only two things worth asking yourself at this point:

  1. Is there something that you think is both compelling and new that you can add to the discussion? (Iā€™m pretty sure this topic has been discussed to death.)
  2. Are you stubborn enough to try to push it through the RFC process? (It seems most people donā€™t get that far.)

Good luck, in either case.

Well, I might have one to add. This is a really, really big thread, so perhaps this has been said, but the solution using anonymous structs wonā€™t play nice with the C FFI. If you want to include the C FFI, I think you have to use a syntax sugar that converts to positional args.

I am probably stubborn enough to push it through the RFC process. I just spent a month refactoring trans, and the number of user-facing features it gained us as of this point is zero. Making and following an RFC thread seems like not so much next to that. Also, I might be able and willing to do the implementation with some pointers in the right direction; presumably the first stages of the compiler are easier than trans.

But I donā€™t want to take it from someone if theyā€™re still working on it, so I thought Iā€™d ask.