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.