Summary
So there has been a lot of discussion, way more than I anticipated.
I will try my best to summarize all the points that have been raised in order to refocus the discussion and make it less intimidating to join in. If you feel like I have left out something important, please let me know.
1. Parsing ambiguity
The syntax proposed in this proposal causes parsing ambiguity with type ascriptions. To be more precise, the colon at call-site can not be used because it is a valid type ascription.
fn foo(pub a: i32) { ... }
foo(a: 42) // <-- This causes problems
A lot of people have agreed that this syntax would have been the most well-suited due to the symmetry with structs. But it is highly unlikely that the type ascription syntax will change at this point and we should look for a different syntax.
Other syntaxes have been proposed:
-
=
Usingfoo(a=5)
is already legal today. It is an assignment in a function call. Even though it is improbable that people do this in practice it would be a breaking change. -
=>
Arrows generally suggest some flow, like->
for return values in functions and=>
in match arms. -
:=
2. How to make an argument named
Most of the participants agree that we should not make all arguments named by default. Because that would make all argument names part of the public API which would add a huge burden to API authors. Every change in an argument's name would become a breaking change. So we need a way to mark arguments as named.
The idea of overloading pub
for this purpose is liked, but other proposals have been made.
Different internal / external name
Like in Swift, we could allow for an internal name and an external name.
fn foo(ext int: i32) { ... }
This addition would make the overloading of the pub
keyword unnecessary. And allows some functions to read better while retaining a meaningful name in the function's implementation.
fn increment(_ value: i32, by increment: i32) {
value + increment
}
increment(value: 3, by: 5)
But, like you can see in the example, it frequently occurs that you want the same external as internal name. In that case you would use an underscore _
to avoid the repetition of name name: i32
3. Alternatives
A lot of people have stated their doubt about the need for named arguments giving alternatives. Here are the alternatives with their advantages and drawbacks:
1. Plain structs
You can already be more explicit with normal structs.
struct MyArgs {
a: i32,
b: i32,
}
fn foo(args: MyArgs) { ... }
fn main() {
foo(MyArgs {a: 42, b: 21});
}
This has the advantage of being possible right now. But it means you have to define a struct for every function that would need named parameters, you can't use the positional form anymore and, worst of all, the user has to import all those argument functions.
2. Builder pattern
struct Foo {
a: i32,
b: i32,
}
impl Foo {
fn new() -> Self { ... }
fn a(self, a: i32) -> Self { ... }
fn b(self, b: i32) -> Self { ... }
}
Foo::new().a(42).b(21)
The builder pattern is a nice API pattern. It allows for default arguments, it scales well, adding an argument is not a breaking change, etc. But is very verbose to write for the API authors, it also requires a struct and only works for methods.
3. Anonymous structs
fn foo({a: i32, b: i32})
foo({a: 42, b: 21})
Anonymous structs have been proposed as an alternative. They are not part of the language now, so they would have to be added. Arguments for this proposal are:
- Less boilerplate than actual structs
- Doesn't have to be imported by the user
- Could provide useful in other parts of the language
Drawbacks include:
- Not being able to use the positional form, "named arguments" would be mandatory
- Not clear if you could implement traits from other crates, think
Default
. How would this be extended for default arguments?
4. How does it work with the type-system?
People are unsure about how named arguments would or should affect the type-system. Would
fn fn foo(a: i32, pub b: i32, pub c: i32) { ... }
be of type
Fn(i32, i32, i32)
or
Fn(i32, b: i32, c: i32)
In other words, should named arguments be part of the type?
The easiest solution would be to not make named parameters part of the type. They would get resolved to their positional alternative before type-checking making them 100% compatible with the current functions. However by not making it part of the type, it can't be used with closures, making them second class citizens. Nobody yet has commented to say if that would be a problem or not.
Making them part of the type would make this feature significantly more complex and nobody has presented any benefits in doing so.
5. Should named arguments be named-only?
In the current proposal, even though a function proposes named arguments, you can still use the positional form.
Swift was taken as an example where, if named arguments are present, you have to use them. This is perceived as annoying and users want to choose for themselves if they need / want to use named arguments in their code base. If the user already uses clear variable names, named arguments can sometimes be redundant causing noise.
Also, this allows arguments to be "promoted" from positional-only to optionally named without a breaking change.
However, other people are unsure about being able to mix both forms simultaneously.
6. Should named arguments be reorderable at call-site?
One of the benefits of named arguments would be to make the order of the arguments irrelevant. Allowing users to provide arguments in the order of their choice. Examples:
fn position(lat: f32, lon: f32) { ... }
position(50.85, 4.35)
position(lat: 50.85, lon: 4.35)
position(lon: 4.35, lat: 50.85)
This provides more freedom to the user and is not a problem because the code is self-documenting. However some feel uneasy about this liberty and would rather follow Swift making the order of named arguments matter because this would make the feature simpler.
In particular, the way in which many implementations of this feature have to pick a "cutoff point" in an argument list between the permutable and non-permutable ("positional") arguments, based on inspecting the labels-provided at call site and labels-allowed at callee, seems like a major comprehensibility hazard.
Also consider this scenario:
The user starts out with the named form.
let lat = 50.85;
let lon = 4.35;
position(lon: lon, lat: lat)
The order is not the same as it would be in positional form, but this doesn't cause any problems. However, if the user now decides to cut the named parameters because it feels redundant in this case, we obtain
let lat = 50.85;
let lon = 4.35;
position(lon, lat)
Which is not the same! Subtle bugs could be introduced this way.
I hope I covered everything, if not comment about it below.