[Pre-RFC] named arguments

Quite a good approach IMHO is to not change anything but use meaningful names for variables and put argument names in comments if needed. i.e.

let mut disp = Display::new(X, Y, W, H, /*obscure_param*/ 666.999);
        disp.set_background(/*r,g,b,a*/ &[0.1, 0.1, 0.2, 1.0]);
        disp.set_title("Example Window");
        disp.set_target_fps(24.0);

let label = String::new(&font, "Hello World", /*justify*/ 0.0);

an advantage of this approach is that the /*name*/ in the fn argument call could be more meaningful than just the actual name.

Also this DIY builder approach makes it clear that the Display instance is now mut. If that's not wanted then there would be a clear line following it..

    ...
            disp.set_target_fps(24.0);
    let disp = disp; // i.e. no longer mut

Note that it doesn't actually use more lines than the chaining dots system, with the advantage that you don't have to look at the source code for Display to figure out what's happening.

What if argument labels were part of the name of the function? This would make them mandatory, and also allow using them to differentiate functions.

impl Rectangle{
  pub fn new(width:f64, height:f64)->Rectangle{...}
  pub fn new(area area: f64, width width: f64)->Rectangle{...}
}

let r1 = Rectangle::new(3.0,4.0);
let r2 = Rectangle::new(area:12.0, width: 3.0);

This is not overloading, because the name of the second function is new(area:width:), while the first is new, and to get a reference to the second, you would have to use Rectangle::new(area:width:), while for the first, Rectangle::new. Also, syntax for the same inner and outer names could be improved. Also, the inner and outer names of the functions can be different, as the best internal names of arguments are not always the best at the callsite.

This would turn using named arguments into an api design decision, and would be opt in, so no backwards compatibility issues.

4 Likes

This is how Swift does it, modulo some source compatibility problems from when parameter labels were part of the type, not the name, discussed here.

(Namely, that any function can be referenced, when not called directly, by its 'base name', e.g. without argument names.)

2 Likes

Yes. However Swift has overloading, so it's a little more complicated, and also, the syntax is a bit better because it was designed from the beginning. Also, swift's apis have been designed with this in mind, so size_of in rust would be size(of:) in swift. As Rust's style has already sort of solidified, it would feel a little different.

1 Like

I really believe that Rust should get named arguments eventually. And it's not about convenience or being more friendly to people coming from dynamic languages, but about helping to prevent bugs and thus making software more reliable (i.e. to be closer to Ada, not Python). And I did encounter bugs which could've been solved by named arguments, for example flat_projection uses lon/lat order instead of a more common lat/lon order (motivation being that lon/lat mirrors X/Y), which I've used automatically out of habit. It took a fair amount of time to find the reason why my program was misbehaving. In RustCrypto we also have encountered this problem when designing AEAD traits. Initially we have used cipher.encrypt(nonce, plaintext, ad), but since authenticated data and plaintext both have type &[u8], there was a real danger that users will use the wrong order, which may have serious implications. We "solved" it with an additional Payload type, enhanced with a cute trick to improve ergonomics.

I've read most of this thread and I think I have a variant which was not proposed yet.

Initially I was in favor of somehow utilizing the struct symmetry for named arguments, but after a fair amount of thinking I've come to a conclusion that it's probably a dead end. There is simply too many issues with generic code and conflicts with type ascription, and attempting to solve them will make feature too big and scary to accept.

Instead I would like to suggest "pub argument blocks" (PAB):

fn foo(a: u32, pub { b: u32, c: u32 }) { .. }
fn bar(pub { a: u32, b: u32 }) { .. }
fn baz(a: u32, pub { b: u32 = 1, c: u32 = SOME_CONST }) { .. }

const fn f() -> u32 { .. }
fn zoo(&self, pub { a: u32 = f()}) { .. }

// foo has type Fn(u32, u32, u32), so
foo(1, 10, 30);
// is equivalent to
foo(1, b = 10, c = 30);
// with type ascriptions
foo(a: u32, c = 1: u32, b = my_b: u32);
// arguments with a default value can be omitted
baz(1);
baz(1, c = 10);
z.zoo();

Why introduce the block instead of using pub before the each argument? First, it's less duplication, and second it will highlight that PAB has some additional constraints and capabilities.

Allowing positional usage of named arguments will allow to add them into existing codebases in a backward compatible fashion (ofc with a MSRV bump). Plus it will limit the scope of the proposal, since for type system function will look as a plain old Fn(u32, u32, u32), without any named type arguments or complications arising from anonymous structs.

PAB will be the only place inside of which default values for arguments will be allowed. Note that only constants can be used as default values. Some may argue that it may be useful to be able to call any function for initializing a default value during runtime (e.g. to allocate some space on the heap for a value), but I think it's a needless complication which does not pull it's weight, plus it may be unintuitive, since AFAIK other languages do not allow such functionality.

PAB will be different from the usual arguments, you will not be able to use patterns, only identifiers, i.e. the following code will be illegal:

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

Of course positional arguments can not be defined or used after named arguments:

// both lines will cause a compilation error
fn f(pub {a: u32}, b: u32) { .. }
foo(b = 10, c = 30, 1);

Now about the calling syntax, as was mentioned many times in this thread, this code is legal right now:

let mut a = 0;
f(a = 1); // `a = 1` evaluates to `()`

I think it should be possible to solve this issue in the next edition (either way, I don't think "named arguments" have a chance to be stabilized before the next edition). In other words, in the next edition inside function call parser will prioritize parsing <ident> = <expr> over parsing expression, while for 2015/2018 edition crates it will work as it does currently.

If for some reason it will not be possible to do it, then there are a bit more ugly options like :=. But I really hope we will be able to use =.

Why not try to work around parsing issues for : instead of =? Since PAB do not use any symmetry with structs, I don't think we should use : for named arguments and in context of function calls leave it for type ascription only.

In future we may even somehow extend PAB with variadic arguments.

Because PAB do allow positional usage of named arguments, some may say that this proposal does not help with bug prevention, since lazy users will omit argument names or they may delete them (which may cause lat/lon issues raised by @graydon). I think a good way to solve this issue will be to mirror #[must_use] and introduce an attribute, which will emit warning if named arguments are used in positional form:

fn foo(pub { lat: f32, lon: f32}) { .. }
#[check_pub_args]
fn bar(pub { lat: f32, lon: f32}) { .. }

// no warnings
foo(1.0, 2.0);
bar(lat: 0.0, lon: 0.0);
// warning: use function argument names
bar(0.0, 0.0);
// for convenience sake we probably should omit warning
// if variable names match argument names, i.e.
// this line will not result in a warning
bar(lat, lon);
// but this one will emit a warning
bar(lon, lat);

Sorry for a relatively rough description of the PAB idea, but I hope was able to explain it well enough for you to understand the general idea and its properties. I think PAB with the attribute solves most of the issues raised in this thread, ergonomic to use and has a very limited scope, which should prevent weird corner cases when used in combination with other Rust features.

13 Likes

@newpavlov: :+1: ... -defaults +mandatory order = an even simpler design

Personally I'd prefer the opposite: require named arguments, for the precise reason to remove ambiguities as a safe(r) default.

For those cases where you require it for backwards-compatibility, or if you simply want default parameter values, add an attribute #[allow_positional]

Hm, your approach may be indeed a better solution considering Rust's spirit. But note, that I propose to issue only warnings, not compilation errors, so you will not need #[allow_positional] for backwards compatibility. Of course it could be a compilation error instead, but I don't think it's worth the churn.

A huge :+1: for this proposal, although as mentioned already I would prefer if the arguments have to be named by default, and with an #[allow_unnamed_args].

1 Like

Why allow default arguments at all? Why wouldn't the reasoning to not implement default arguments not apply here?

The post describes the direction in which I would like for Rust to move and this includes default argument values (a.k.a optional arguments). Several people even think that named arguments don't have enough motivation on their own without default arguments (though I disagree with this opinion), for example:

In the real RFC I think default arguments should go to "possible future extensions" and probably proposed separately in its own RFC later.

Can you please re-iterate arguments against default arguments for informational purposes? I am sure default argument proposals were discussed extensively, but I haven't read those discussions and AFAIK those arguments haven't been raised in this thread.

2 Likes

I thought that it was an ABI thing for why rust didn't have default arguments, but I can't find a reference to it, nor can I find any lang team members who were actually against it in general, so I may have been mistaken. It looks like default arguments were just never implemented:

Optional arguments was cited as a major reason why the other major default argument proposal wasn't accepted (default optional arguments would solve the issues and do more, like what you are suggesting).

Relatedly, I wonder if instead of adding named arguments and default arguments as-is, it would be possible to extend structural records themselves with a Default impl that could allow code like

foo({ arg1: bar, ..Default::default() })

which would then solve both problems with structural records and save us of the problems "proper" named arguments usually cause.

7 Likes

My take on this using default field values + structural records is that we can have:

type Config = { height: u32 = 1080, width: u32 = 1920 };

fn open_window(config: Config) {
    // logic...
}

open_window({ height: 900, .. });

(..Default::default() would also work with just structural records)

16 Likes

My take on this using default field values + structural records is that we can have:

I like this direction a lot.

I still would like to see type inference for struct literals. I would love being able to replace the 'type Config' in your example with just a 'struct Config' and still being able to create a 'Config' from just '{ height: 900, .. }'.

I think currently type ascription makes this syntax ambiguous, but have this kind of type inference for structs would be IMHO a even more general solution.

Meh. I'd rather have a form of struct name elision, so you could still write

open_window({ height: 900, .. });

but Config would just be a regular struct:

struct Config { height: u32 = 1080, width: u32 = 1920 }
6 Likes

There's no technical reason why you cannot have both. :slight_smile:

6 Likes

I can't emphasize enough of how much I like this idea and approach.

3 Likes

In a Unity / C# project at my previous company, I recommended we use named arguments for any literal, especially Boolean. With point free style, you can get away with unnamed arguments in low arity functions, but in Unity, or any complex system, I find it more difficult. After adding named arguments, we could easily see what all the True and 1.0s were for. In C#, they are optional, so it’s up to the discretion of the author / reviewer. I’m surprised there has been no mention of C# here, just some Python.

1 Like

To be fair to Rust, it's unidiomatic to use true and false outside of FFI. You're supposed to define enums instead, since their variants are self-describing, most argument transposition errors can get caught at compile time, and they're easily extensible if you decide down the road that you need a third or fourth variant.

3 Likes